Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOM Parts] Add new declarative syntax and update iterative proposal #1023

Open
wants to merge 2 commits into
base: gh-pages
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 0 additions & 73 deletions proposals/DOM-Parts-Declarative-Template.md

This file was deleted.

131 changes: 131 additions & 0 deletions proposals/DOM-Parts-Declarative.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# DOM Parts Declarative API

This proposal covers the declarative API for creating a DOM part within a
`<template>` element and main document HTML.

## Proposal

Double curly braces `{{}}` provide markers for DOM parts.

In the most basic level, this proposal can produce the three DOM parts:
`NodePart`, `AttributePart`, `ChildNodePart`.

### Basic Examples

Suppose we had the following template in some HTML extension template languages
where `{name}` and `{email}` indicated locations of dynamic data insertion:

```html
<section>
<h1 id="name">{name}</h1>
Email: <a id="link" href="mailto:{email}">{email}</a>
</section>
```

And the application has produced an HTML `<template>` with the following
content:

```html
<template>
<section>
<h1 id="name">{{}}</h1>
Email: <a id="link" href="{{}}">{{}}</a>
</section>
</template>
```

This will create a `ChildNodePart` attached to `<h1>` with no content, an
`AttributePart` connected to `href`, and a `ChildNodePart` connected to `<a>`
with no content.

A framework could fetch these parts using the `getPartRoot()` on the
[`DocumentFragment`](./DOM-Parts-Imperative.md#retrieving-a-documentpartroot)
and then calling [`getParts()`](./DOM-Parts-Imperative.md#getparts).

## Enablement

For any DOM node, including `<template>`, a new `parseparts` attribute is
introduced that indicates to the parser it should parse DOM part tags as DOM
parts.

```html
<div parseparts></div>
```

Even for `innerHTML` use cases, only DOM nodes that are wrapped DOM with the
`parseparts` attribute will use declarative parts.

## Node parts

For node parts, a `{{}}` tag could be provided as an attribute.

```html
<template>
<section {{}}></section>
</template>
```

Would create a `NodePart` for `<section>`.

## Partial attributes

Allowing `{{}}` inside an attribute works the same as a
[partial attribute update](./DOM-Parts-Imperative.md#partial-attribute-updates),
in that it will create an `AttributePart` for the entire attribute, but it will
have multi-valued `value` property.

```html
<template>
<section>
<h1 id="name">{{}}</h1>
Email: <a id="link" href="mailto:{{}}">{{}}</a>
</section>
</template>
```

The `AttributePart` for `href` would have `statics` equal to `['mailto:', '']`.
Empty string values are provided for any markers without default content.

## Default Values

To provide default values for any part, a part can be split into a start `{{#}}`
and finish `{{/}}` indicators.

```html
<template>
<section>
<h1 id="name">{{#}}Ryosuke Niwa{{/}}</h1>
Email:
<a id="link" href="mailto:{{#}}[email protected]{{/}}">
{{#}}[email protected]{{/}}
</a>
</section>
</template>
```

## Names and Metadata

Templating systems may need to serialize data about the nodes they are marking
into the processing instructions. Or at the very least parts could be named so
that they are easier to fetch.

```html
<div>{{email data="foo"}}</div>
```

This could be exposed on the imperative API to be consumed in JavaScript by
application logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to punt, but have we considered giving the metadata as a string rather than a string array? People may want to use a variety of metadata encodings, e.g. JSON, and it'd be easier to just pass along the raw contents and let them split by spaces if they'd like to do that.

Related, but we'll want to specify how to escape }} inside of part syntax, and how to escape {{ inside parsepart html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah it's not clear, but the reason metadata is a string is not because we split the string, it's because you might actually have metadata in two places for the same part.

{{# metadata1}}foo{{/ metadata2}}

I'll call this out.


For parts that are split between opening and closing, it's possible to have
multiple metadata values which are included as two elements in the `metadata`
field.

```
{{# metadata1}}default value{{/ metadata2}}
```

## Choice of marker

The `{{}}` and `{{#}}{{/}}` are reasonable DOM part markers, but this is open to
proposals. It's possible to even allow the page to decide ewhat their markers

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/ewhat/what

should be.
410 changes: 153 additions & 257 deletions proposals/DOM-Parts-Imperative.md
Original file line number Diff line number Diff line change
@@ -8,43 +8,71 @@ A DOM part is represented by the `Part` interface and its sub interfaces.

```webidl
interface Part {
attribute any value;
void commit();
attribute any value;
readonly attribute PartRoot root;
readonly attribute FrozenArray<DOMString> metadata;
void commit();
};
dictionary PartInit {
FrozenArray<DOMString> metadata;
};
interface NodePart : Part {
constructor(Node node);
readonly attribute Node node;
constructor(PartRoot root, Node node, optional PartInit init = {});
readonly attribute Node node;
};
interface AttributePart : Part {
constructor(Element element, DOMString qualifiedName, DOMString? namespace);
readonly attribute DOMString prefix;
readonly attribute DOMString localName;
readonly attribute DOMString namespaceURI;
constructor(
PartRoot root,
Element element,
DOMString qualifiedName,
optional DOMString? namespace = null,
optional Array<DOMString> statics = [],
optional PartInit init = {});
readonly attribute DOMString prefix;
readonly attribute DOMString localName;
readonly attribute DOMString namespaceURI;
readonly attribute DOMString rawName;
readonly attribute FrozenArray<DOMString> statics;
};
interface ChildNodePart : Part {
constructor(Node node, Node? previousSibling, Node? nextSibling);
readonly attribute Node? previousSibling;
readonly attribute Node? nextSibling;
constructor(
PartRoot root,
optional Node? previousSibling = null,
optional Node? nextSibling = null,
optional PartInit init = {});
readonly attribute Node? previousSibling;
readonly attribute Node? nextSibling;
readonly attribute FrozenArray<Node> children;
};
interface DocumentPartRoot {
};
```

The `Part` has one property named "value" of
[_any_ type](https://heycam.github.io/webidl/#idl-any).
`Part` points to a specific type of DOM, most commonly a `Node` or collection of
sibling nodes. Depending on the type of `Part`, it has various properties that
expose more information.

When a new value is set or assigned to a DOM part, the change does not
immediately reflect back to the corresponding
[node](https://dom.spec.whatwg.org/#concept-node), its
Each `Part` has a `value` that can be updated. When a new value is set or
assigned to a DOM part, the change does not immediately reflect back to the
corresponding [node](https://dom.spec.whatwg.org/#concept-node), its
[attributes](https://dom.spec.whatwg.org/#concept-attribute), or its
[properties](<(https://tc39.es/ecma262/#sec-object-type)>). Instead, the new
value is staged to be later committed in a batch. This batching reduces the
runtime overhead of constantly returning control back from browser's
implementation to JavaScript between each DOM mutation and allows browser
engine's to avoid or batch certain sanity checks and housekeeping tasks.

In the most basic level, this proposal consists of three DOM parts:
In the most basic level, this proposal consists of four DOM parts:

1. `NodePart` represents a single
[node](https://dom.spec.whatwg.org/#concept-node).
@@ -54,8 +82,11 @@ In the most basic level, this proposal consists of three DOM parts:
[child](https://dom.spec.whatwg.org/#concept-tree-child)
[nodes](https://dom.spec.whatwg.org/#concept-node) of a node which can be
[replaced](https://dom.spec.whatwg.org/#concept-node-replace).
1. `DocumentPartRoot` represents a
[document](https://dom.spec.whatwg.org/#concept-document) or
[document fragment](https://dom.spec.whatwg.org/#interface-documentfragment).

### Basic Examples
### Basic examples

Suppose we had the following template in some HTML extension template languages
where `{name}` and `{email}` indicated locations of dynamic data insertion:
@@ -87,9 +118,13 @@ as follows:
```js
const name = staticContent.getElementById("name");
const link = staticContent.getElementById("link");
const namePart = new ChildNodePart(name);
const emailPart = new ChildNodePart(link);
const emailAttributePart = new AttributePart(link, "href");
const namePart = new ChildNodePart(document.getPartRoot(), name);
const emailPart = new ChildNodePart(document.getPartRoot(), link);
const emailAttributePart = new AttributePart(
document.getPartRoot(),
link,
"href"
);
```

Then assigning values as follows will update the DOM:
@@ -112,141 +147,66 @@ The resultant DOM will look like this:
</section>
```

## Part Groups

DOM parts need grouping and ownership to provide batching and to enable parts
created declaratively to be retrieved and updated by JavaScript.

### Option 1. `PartGroup`

One option is to make the concept of DOM part group a real DOM
[interface](https://heycam.github.io/webidl/#idl-interfaces):

```webidl
interface PartGroup {
constructor(sequence<Part> parts);
readonly attribute FrozenArray<Part> parts;
void commit();
}
```

Note that if we allow a single `Part` to belong to multiple `PartGroup`s, the
first `PartGroup` which commits the changes would apply the mutations. In
effect, this allows
non-[partitioned](https://en.wikipedia.org/wiki/Partition_of_a_set) grouping of
`Part` objects to be committed together.

There is also a question of how mutable parts should be, and whether a
`PartGroup` can appear as a part of another `PartGroup` for nested template
instances or not. It doesn't make much sense for the list of _DOM parts_
associated with `PartGroup` to get mutated after we've started committing things
but there certainly is a room for adding or removing _DOM parts_ based on new
input or state.

If we made the relationship between DOM parts and `PartGroup` not dynamically
mutable, users of this API could still create a new `PartGroup` each time such a
mutation would have needed instead.

The `parts` order would be the order in which DOM part was inserted to the
`PartGroup`. Although there could be multiple DOM parts which reference the same
element in different parts of the array, that doesn't necessarily pose an
obvious issue other than a slight inefficiency in batching certain DOM
operations.

### Option 2. `DocumentPartGroup`
## Retrieving a `DocumentPartRoot`

A dynamic list of parts could be maintained at the `document` (and document
fragment) level that would allow fetching all parts.
For every document or document fragment, a new method `getPartRoot()` is added
that returns a `DocumentPartRoot`.

```webidl
interface DocumentPartGroup {
readonly attribute Array<Part> parts;
void commit();
}
partial interface Document {
readonly attribute DocumentPartGroup documentPart;
DocumentPartRoot getPartRoot();
}
```

The list of parts would be cached initially on render, and then invalidated for
lazy recalculation for any new `Part` that was declaratively or imperatively
added to the `document`.

The `parts` array would be in DOM-order. The exact algorithm for how to keep the
`parts` array up to date with the `document` is an open question.

### Option 3. `ChildNodePart` is a `PartGroup`
To make `DocumentPartGroup` more performant and to provide better structure to
`Part` relationships for a more optimal DOM walk, `ChildNodePart` could itself
be a `PartGroup`, and would contain any `Part` objects that were nested inside
its range, and child `parts` would not be part of any parent `ChildNodePart` or
`DocumentPartGroup`.

```webidl
interface ChildNodePart {
readonly attribute Array<Part> parts;
void commit();
partial interface DocumentFragment {
DocumentPartRoot getPartRoot();
}
```

The `parts` array would be in DOM-order. The exact algorithm for how to keep the
`parts` array up to date with the DOM subtree rooted by the `ChildNodePart` is
an open question.
## Nested parts and cloning with `PartRoot`

## Cloning Parts
`DocumentPartRoot` and `ChildNodePart` both implement `PartRoot` and contain
nested parts.

Cloning parts along with the nodes they refer to is a major use case for DOM
parts.
```
interface mixin PartRootMixin {
FrozenArray<Part> getParts();
PartRoot clone();
};
### Option 1: `cloneWithParts`
ChildNodePart includes PartRootMixin;
DocumentPartRoot includes PartRootMixin;
One option would to add a new API to `Node`:
typedef (DocumentPartRoot or ChildNodePart) PartRoot;
```

```webidl
partial interface Node {
NodeWithParts cloneWithParts(optional CloneOptions options = {});
};
### `getParts()`

dictionary CloneOptions {
boolean deep = true;
Document? document;
PartGroup? partGroup;
};
`getParts()` returns an array of parts that are contained within a `PartRoot`.

dictionary NodeWithParts {
Node node;
PartGroup? partGroup;
};
```
The array of parts would be cached initially on render, and then invalidated for
lazy recalculation for any new `Part` that was declaratively or imperatively
added to the `document` or for DOM node additions or removals.

Here, we're proposing a slightly nicer API by combining
[`importNode`](https://dom.spec.whatwg.org/#dom-document-importnode) and
[`cloneNode`](https://dom.spec.whatwg.org/#dom-node-clonenode) and making the
[cloning](https://dom.spec.whatwg.org/#concept-node-clone) deep by default.
The array of parts would be in DOM-order. The exact algorithm for how to keep
the array up to date with the `document` is up to the user-agent, but is
invalidated anytime the user-agent detects changes to the DOM that could
invalidate the array.

### Option 2: `cloneWithParts` on `DocumentPart` and `ChildNodePart`
#### Open nested part questions

Since `DocumentPart` and `ChildNodePart` both are rooted at a specific node, the
clone semantics are clearer:
The precise specification of invalidation, caching, etc. is still yet to be
described.

```webidl
partial interface DocumentPart {
NodeWithParts clone();
}
### `clone()`

partial interface ChildNodePart {
NodeWithParts clone();
}
```
`clone()` deep clones the `PartRoot`, its nested parts, and the DOM nodes. It
returns a new `PartRoot` of the same type as the callee.

### Other Cloning Questions
#### Open cloning questions

There are some questions here as well. What happens to the current values of DOM
parts? Do we allow DOM parts to have some non-initial values and do we clone
those values as well? If so, what do we do with proposed extensions like
`PropertyPart` / `CustomPart`?
There are some edge cases that are worth thinking about, like whether
un-committed values of parts are cloned or whether invalid parts should be
cloned.

## Partial Attribute Updates

@@ -256,156 +216,87 @@ had initially contained `mailto:` before `{email}` but we could not capture this
prefix in the attribute value because `AttributePart` could only set the whole
attribute value.

There are a few options for how to support the use case of updating attributes
with embedded static content such as `mailto:`

### Option 1. Create Multiple `AttributePart`s Together

In this approach, `AttributePart` gets a new static function which creates a
list of `AttributePart`s which work together to set a value when the values are
to be committed:
Instead of `AttributePart` having a single string `value`, it could optionally
take an `Array` that contains values that should be concatenated together. This
allows updating individually parts of the attribute without needing to serialize
the entire string.
Comment on lines +219 to +222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Instead of `AttributePart` having a single string `value`, it could optionally
take an `Array` that contains values that should be concatenated together. This
allows updating individually parts of the attribute without needing to serialize
the entire string.
Instead of `AttributePart` having a single string `value`, it could instead
take an `Array` with values that will be interleaved with those from the `strings` array.

If we go with the strings design, we might want to require that values be an array, to be interleaved with the strings. I suppose we could take a non-array value if there's only two strings, but in practice I expect template systems to just always pass through an array here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to think more about the DX of this.


```js
const [firstName, lastName] = AttributePart.create(element, "title", null, [
null,
" ",
null,
]);
// Syntax to be improved. Here, a new AttributePart is created between each string.
const part = AttributePart(document.getPartRoot(), element, "href");
part.value = ["mailto: ", email];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should think about making this behavior consistent and intuitive across the case where there are statics vs when there aren't. We also need to work with trusted types, so that we can set part.value to a trusted type in a page where trusted types are enforced.

One proposal:

An attribute is said to be fully controlled by an AttributePart's value if the statics are ['', '']. In that case, commit behaves like:

  #commitFullyControlled() {
    const value = Array.isArray(this.value) ? this.value[0] : this.value;
    if (value == null || value === false) {
      this.element.removeAttributeNS(this.namespaceURI, this.localName);
      return;
    }
    this.element.setAttributeNS(this.namespaceURI, this.localName, value);
  }

Note: if value is an array, we use its first element. This is to be consistent with the behavior when there are nonempty statics. null, undefined, and false when set on a fully controlled attribute remove it from the element.

If the attribute is not fully controlled by the value, then commit behaves like:

  #commitWithStatics() {
    const value = Array.isArray(this.value) ? this.value : [this.value];
    let pieces = [this.statics[0]];
    for (let i = 1; i < statics.length; i++) {
      pieces.push(value[i - 1] ?? '', this.statics[i]);
    }
    return pieces.join('');
  }

Note: if the values array is too short, then the ?? '' will write it as empty string, and excess elements are ignored.

And for completeness:

  commit() {
    if (this.statics.length === 2 && this.statics[0] === '' && this.statics[1] === '') {
      this.#commitFullyControlled();
    } else {
      this.#commitWithStatics();
    }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the wording a bit in this section to more cleanly spell out how statics and values fit together.

I'm not sure on statics = ['', ''] being the boundary condition. Why not just statics = undefined in the case where the AttributePart was created without statics? We can throw an Error during construction if someone tries to create an AttributePart with a static array that is less than 2 in length.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

['', ''] may be more elegant, but undefined may be more performant, and I'd prefer the more performant option. No objection from me for undefined

```

- **Pros**: Simplicity.
- **Cons**: Coming up with a nice syntax to create a sequence of AttributePart
and string can be tricky.

### Option 2. Introduce `AttributePartGroup`

In this approach, we group multiple `AttributePart`s together by creating an
explicit group:
Additionally, `AttributePart` can be constructed with a list of static parts. If
there are any static parts, the first static part will precede the first value,
the second static part will precede the second value, and so on until the final static value is reached.

```js
const firstName = new AttributePart();
const lastName = new AttributePart();
const group = AttributePartGroup(element, "title");
group.append(firstName, " ", lastName);
```

This is morally equivalent to option 1 except there is an explicit grouping
step.

- **Pros**: Nicer syntax by the virtue of individual "partial" `AttributePart`'s
existence at the time of grouping. Code that assigns values to `AttributePart`
only needs to know about `AttributePart`
- **Cons**: More objects / complexity. `AttributePart` will have two modes.

### Option 3. Introduce `AttributePartFragment`

Unlike option 2, this creates `AttributePartFragment`s from `AttributePart`,
meaning that `AttributePart` in option 3 plays the role of `AttributePartGroup`
in option 2:

```js
const firstNamePartial = new AttributePartFragment();
const lastNamePartial = new AttributePartFragment();
const part = AttributePart(element, "title");
part.values = [firstNamePartial, " ", lastNamePartial];
const part = AttributePart(document.getPartRoot(), element, "href", undefined, [
"mailto: ",
]);
part.value = email;
```

- **Pros**: Nicer syntax by the virtue of individual `AttributePartFragment`'s
existence at the time of grouping. `AttributePart` just knows one thing to do:
to set the whole content attribute value.
- **Cons**: More objects / complexity. Code that uses a template has to deal
with two different kinds of objects: `AttributePartFragment` and
`AttributePart`.

### Option 4. Support arbitrary JavaScript objects

One way of punting is to support arbitrary JavaScript objects as `value` that
conform to some interface. This interface could be as simple as `toString()`, or
could use `Symbol` to determine how to populate attributes. This would allow
code that wanted to represent partial attributes, but would maintain the
property that parts represent nodes or groupings of nodes.
This API may be strengthened further to take in some parsed template string,
which would allow the browser to determine which parts of the attribute were
compile-time constants. This could allow using tagged template literals to pass
static content to the browser.

```js
class TitleAttributeValue {
constructor() {
this.firstName = "";
this.lastName = "";
}

toString() {
return `${this.firstName} ${this.lastName}`;
}
}

const part = AttributePart(element, "title");
part.value = new TitleAttributeValue();
part.value.firstName = "Ryosuke";
part.value.lastName = "Niwa";
const part = AttributePart(
document.getPartRoot(),
element,
"href",
undefined,
attribute`mailto: ${0}`
);
part.value = [email];
```

- **Pros** No need for new `PartialAttributePart` or `AttributePart`
coordination. Does not block a future API object that represents partial
attributes.
- **Cons** Need a way to represent partial attributes that are declaratively
defined.
At this time there is no specific native representation of templates that could
be inspected to determine with certainty which strings were compile time
constants, but if some a representation were to exist, this API could provide
better security for attributes that were sens

## Sibling `ChildNodePart`s

Like partial attribute updates, when there are multiple points of interests
under a single [parent](https://dom.spec.whatwg.org/#concept-tree-parent)
[node](https://dom.spec.whatwg.org/#concept-node), and they're next to each
other, index does not adequately describe a specific location in the DOM when
other parts [insert](https://dom.spec.whatwg.org/#concept-node-insert) or
other, previous and next sibling does not adequately describe a specific
location in the DOM when other parts
[insert](https://dom.spec.whatwg.org/#concept-node-insert) or
[remove](https://dom.spec.whatwg.org/#concept-node-remove)
[children](https://dom.spec.whatwg.org/#concept-tree-child).

### Option 1. Create Multiple `ChildNodePart`s Together
### Option 1. Create multiple `ChildNodePart`s together

In this approach, `ChildNodePart` gets a new static function which creates a
list of `ChildNodePart`s which work together to set a value when the values are
to be committed:

```js
const [firstName, lastName] = ChildNodePart.create(element, null, null, [
const [firstName, lastName] = ChildNodePart.create(
document.getPartRoot(),
element,
null,
" ",
null,
]);
[null, " ", null]
);
```

- **Pros**: Simplicity.
- **Cons**: Coming up with a nice syntax to create a sequence of `ChildNodePart`
and string can be tricky.

### Option 2. Introduce `ChildNodePartGroup`

In this approach, we group multiple `ChildNodePart`s together by creating an
explicit group:

```js
const firstName = new ChildNodePart();
const lastName = new ChildNodePart();
const group = ChildNodePartGroup(element, null, null);
group.append(firstName, " ", lastName);
```

This is morally equivalent to option 1 except there is an explicit grouping
step.

- **Pros**: Nicer syntax by the virtue of individual "partial" `ChildNodePart`s
existence at the time of grouping. Code that sets new children to
`ChildNodePart`s only needs to know about `ChildNodePart`.
- **Cons**: More objects / complexity. `ChildNodePart` will have two modes.

### Option 3. Introduce `PartialChildNodePart`
### Option 2. Introduce `PartialChildNodePart`

This creates `PartialChildNodePart` from `ChildNodePart`:

```js
const firstNamePartial = new PartialChildNodePart();
const lastNamePartial = new PartialChildNodePart();
const part = ChildNodePart(element, null, null);
const part = ChildNodePart(document.getPartRoot(), element, null, null);
part.values = [firstNamePartial, " ", lastNamePartial];
```

@@ -415,26 +306,31 @@ part.values = [firstNamePartial, " ", lastNamePartial];
with two different kinds of objects: `PartialChildNodePart` and
`ChildNodePart`.

### Option 4. Allow `nextSibling` and `previousSibling` to point to another `ChildNodePart`
### Option 3. Allow `nextSibling` and `previousSibling` to point to another `ChildNodePart`

We would update `ChildNodPart` interface as follows and allow `previousSibling`
and `nextSibling` to point to another `ChildNodePart` as well as `Node`:
Update `ChildNodPart` interface as follows and allow `previousSibling` and
`nextSibling` to point to another `ChildNodePart` as well as `Node`:

```webidl
interface ChildNodePart : Part {
constructor(Node node, (Node or ChildNodePart)? previousSibling, (Node or ChildNodePart)? nextSibling);
readonly attribute Node parentNode;
readonly attribute (Node or ChildNodePart)? previousSibling;
readonly attribute (Node or ChildNodePart)? nextSibling;
constructor(
PartRoot root,
optional (Node or ChildNodePart)? previousSibling = null,
optional (Node or ChildNodePart)? nextSibling = null,
optional PartInit init = {});
readonly attribute (Node or ChildNodePart)? previousSibling;
readonly attribute (Node or ChildNodePart)? nextSibling;
readonly attribute FrozenArray<Node> children;
};
```

Then we can insert two consecutive `ChildNodePart`s by relating them in the
constructor as follows:

```js
const firstName = new ChildNodePart(element);
const lastName = new ChildNodePart(element, firstName);
const firstName = new ChildNodePart(document.getPartRoot(), element);
const lastName = new ChildNodePart(document.getPartRoot(), element, firstName);
```

Note that `lastName` takes `firstName` as the previous sibling but `firstName`
@@ -445,8 +341,8 @@ of previous/next sibling of other parts in the constructor.
An alternative is to add an explicit API to chain multiple parts togethers:

```js
const firstName = new ChildNodePart(element);
const lastName = new ChildNodePart(element);
const firstName = new ChildNodePart(document.getPartRoot(), element);
const lastName = new ChildNodePart(document.getpartRoot(), element);
ChildNodePart.chain(firstName, lastName);
```

@@ -460,8 +356,8 @@ Instead of worrying about coordination, the imperative API could throw an
`Error` if users constructed two `ChildNodePart`s that overlapped.

```js
const firstName = new ChildNodePart(element);
const lastName = new ChildNodePart(element); // throws an Error.
const firstName = new ChildNodePart(document.getPartRoot(), element);
const lastName = new ChildNodePart(document.getPartRoot(), element); // throws an Error.
```

Like in option 4, new APIs like `chain` or an a `append` could be added that
@@ -507,8 +403,8 @@ always are scoped to a single `Node`'s children.

These invisible markers would not be `Node`s and so would be backwards
compatible. DOM mutations would follow shrinking `Range` semantics, meaning the
`ChildNodePart` would remove any `Node` that is removed in between the markers,
and would only add a `Node` if it is added in between the markers.
`ChildNodePart` would remove any `Node` that is removed in between the
markers,and would only add a `Node` if it is added in between the markers.

## `PropertyPart` and `CustomCallbackPart`

2 changes: 1 addition & 1 deletion proposals/DOM-Parts.md
Original file line number Diff line number Diff line change
@@ -103,4 +103,4 @@ There are a few component pieces to this proposal, so it is easiest to split it
into multiple documents.

1. [Imperative API to construct DOM parts](./DOM-Parts-Imperative.md)
1. [Declarative API to construct DOM parts in `<template>`](./DOM-Parts-Declarative-Template.md)
1. [Declarative API to construct DOM parts](./DOM-Parts-Declarative.md)