-
Notifications
You must be signed in to change notification settings - Fork 382
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
base: gh-pages
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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. | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: s/ewhat/what |
||
should be. |
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; | ||||||||||||||
tbondwilkinson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
readonly attribute DOMString localName; | ||||||||||||||
readonly attribute DOMString namespaceURI; | ||||||||||||||
tbondwilkinson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If we go with the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 One proposal: An attribute is said to be fully controlled by an AttributePart's value if the statics are #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 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 And for completeness: commit() {
if (this.statics.length === 2 && this.statics[0] === '' && this.statics[1] === '') {
this.#commitFullyControlled();
} else {
this.#commitWithStatics();
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||
``` | ||||||||||||||
|
||||||||||||||
- **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 | ||||||||||||||
tbondwilkinson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
## 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` | ||||||||||||||
|
||||||||||||||
|
There was a problem hiding this comment.
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{{
insideparsepart
htmlThere was a problem hiding this comment.
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.
I'll call this out.