THIS REPOSITORY IS NOW ARCHIVED. ALL DOM PARTS WORK IS BEING DONE IN https://github.com/WICG/webcomponents
In many applications and frameworks, JavaScript code needs to locate and mutate a set of "nodes of interest." The current methodology for finding "nodes of interest" is either a full DOM tree walk or DOM queries, and for updating either that walk is repeated or the "nodes of interest" are then retained in JavaScript data structures or as properties on the DOM objects.
There are two major drawbacks to these solutions:
- DOM mutating methods like
clone()
are not aware of in-memory refereces to cloned nodes or special JavaScript properties. They can only clone the HTML content itself. - The DOM walks for locating nodes often occur immediately after the browser has already performed that same DOM walk, for example during HTML rendering of the document or
<template>
nodes.
The browser could assist in locating, storing, and updating these nodes with new primitives that identify nodes and ranges of nodes at parse time and an imperative API to retrieve, walk, and update these nodes.
Summary of Use Cases
Template-based Client-side Rendering: Locating and updating nodes in cloned <template>
HTML
- Lit: Visits placeholders in
<template>
cloned content. - SolidJS: Visits placeholders in
<template>
cloned content. - Angular: Interested in Lit + SolidJS approach
- Wiz (Google Internal): Interested in Lit + SolidJS approach
Server-side Rendering and Hydration: Locating and updating nodes in main document HTML
- Vue, Svelte, others: Needs to visit DOM nodes to add event listeners, then same use case as template-based client-side rendering. Some frameworks like Qwik only hydrate parts of the page that have interaction.
- Wiz (Google Internal): Locates jscontroller tagged nodes. Locates jsname tagged nodes for jscontrollers.
Deferred Server-side Rendering: Declaratively marking locations to be used to later slot in content.
- React: Identify a location in the DOM that content that is rendered later should be automatically inserted into.
- Deferred Rendering (Google Search): Identify a location in the DOM that content that is rendered later should be automatically inserted into the page, (display: none content, e.g.).
Component Representation: Representing components that do not have a clear reprensetation in the DOM.
- React: A component may not be rooted with a single element root and may instead be rooted with 0 or more top-level nodes. No way to represent this in HTML and get behaviors like event listening, DOM measurement.
- Wiz (Google Internal): Component ownership may skip into child components (comparable to
<template>
slots).
These are the potential requirements for a new browser API that solved the above use cases:
- Markers do not affect rendering.
- Markers do not affect tree hierarchy.
- Markers can mark a single node.
- Markers can mark a range of nodes.
- Markers can mark attributes.
- Markers can mark a range of characters within an attribute.
- Markers can be nested and have hierarchy, and have 1 parent and 0 or more children.
- Markers are performantly preserved after a DOM clone.
- Markers are performantly preserved after DOM mutations.
- Markers are fast to find using an imperative API.
- Markers can be imperatively created with JavaScript.
- Markers can be declaratively created with HTML.
- The HTML to create a marker does not require a new document parsing mode to parse.
- The HTML to create a marker must be emittable by servers using HTML-compliant serializers.
- The HTML to create a marker is "universal", and can be output inside or outside of tags.
- The HTML to create a marker is ergonomic and directly writable by developers.
- There should be only one syntax for declaratively creating a marker.
- Browsers can use markers for deferred DOM insertion.
- Browsers can use markers for component features like event listening.
Requirement | CSR | SSR | Deferred DOM | Declarative CE | Component |
---|---|---|---|---|---|
Do not affect rendering | X | X | X | X | X |
Do not affect tree hierarchy | X | X | X | X | X |
Mark a single node | X | X | X | X | X |
Mark a range of nodes | X | X | X | X | X |
Mark attributes | ~ | ~ | ~ | ||
Mark text in attributes | ~ | ~ | ~ | ||
Markers have hierarchy | X | X | X | X | |
Preserved after clone | X | ||||
Preserved after DOM mutations | X | X | X | X | X |
Performant to retrieve in JS | X | X | ~ | X | |
Imperative syntax | X | ~ | |||
Declarative syntax | X | X | X | X | |
> No new document mode | X | X | X | X | X |
> Marker is valid HTML | X | X | X | X | X |
> Marker in place | |||||
> Ergonomic syntax | ~ | ~ | ~ | ~ | ~ |
> One syntax |
The below DOM parts proposal uses "parts" as the markers into the DOM and satisfies some of the requirements.
- There is a new clone API that preserves DOM parts.
- The browser keeps DOM parts alive as long as the elements they mark are alive.
- DOM parts are accessible from the document, but it's not always constant time because the browser would defer determining DOM order of parts until the first access.
- DOM parts enable accessing DOM nodes, so it's as fast as a normal DOM update, but not faster.
NodePart
marks a single node.NodePart
can mark a text node andChildNodePart
could wrap a text node(s).ChildNodePart
marks a range of sibling nodes.- There is no ability to mark attributes.
- There is no ability to mark a range of characters within an attribute.
ChildNodePart
contains parts, as doesDocumentPart
.- DOM parts produce comments, which do not affect rendering.
- DOM parts produce comments, which do not affect tree hierarchy.
- DOM part processing instruction API creates DOM parts.
- Some HTML-compliant serializers cannot produce processing instructions
- There is no new document mode to parse DOM parts.
- There is only one processing instruction syntax.
- Processing instructions are not valid inside tags
- Processing instructions are arguably not ergonomic
- DOM parts would enable such other APIs, but does not propose them.
- DOM parts includes an imperative API.
Processing instructions will allow caching nodes of interest during parsing. An imperative API will allow maintaining a live tree of nodes of interest in the DOM. The imperative API is a modification/addition to the original DOM Parts proposal. For information on how this proposal differs from the original DOM Parts proposal, see this explainer. For information about the polyfill, see this explainer.
The improvement here requires there be some way to request that the parser preserve pointers to parts of the DOM, but that once these requests to the parser have been parsed, are not preserved in the DOM and have no influence over it. Processing instructions are an existing well-known quantity in terms of the spec, so it is a convenient write target for this new feature.
This proposal introduces two new processing instructions. An example:
<html>
<section>
<h1 id="name"><?child-node-part?><?/child-node-part?></h1>
Email:
<?node-part metadata?><a id="link"></a>
</section>
</html>
There are two ways to identify parts:
<?node-part?>
which creates a part attached to the next sibling node.<?child-node-part?>
which begins a part<?/child-node-part?>
which ends the part and can optionally wrap content.
// To retrieve the active list of parts, parsed from HTML or imperatively.
const documentPart = document.getDocumentPart();
const parts = documentPart.getParts();
// If you want to add a new part
const nodePart = new NodePart(document.getElementById("your-element"));
// Or a ChildNodePart
const childNodePart = new ChildNodePart(
nodePart.node.children[3],
nodePart.node.children[5]
);
// This part would appear in childNodePart's parts, rather than the document part.
const nestedNodePart = new NodePart(nodePart.node.children[4]);
// Updated to reflect the new imperatively added parts.
const updatedParts = documentPart.getParts();
Once parsed, these parts are contained in PartRoot
objects, which are accessible off of Document
or DocumentFragment
nodes.
interface PartRoot {
// In-order DOM array of parts.
getParts(): Part[];
}
class DocumentPart implements PartRoot {
constructor(document: Document | DocumentFragment) {}
getParts(): Part[];
clone(): DocumentPart;
}
declare global {
interface Document {
getDocumentPart(): DocumentPart;
}
interface DocumentFragment {
getDocumentPart(): DocumentPart;
}
}
The browser does fancy bookkeeping to ensure that getParts()
is live, but it may defer some work to actual calls, as getElementById()
does.
DocumentPart
also has a clone method which also clones the parts.
The base interfaces for all parts is:
interface Part {
readonly root?: PartRoot;
readonly metadata: string[];
disconnect(): void;
}
root
is a pointer to the PartRoot
this part is in. metadata
is additional parsing metadata attached to the Part
. disconnect()
removes the Part from its root.
A NodePart
is constructed for <?node-part?>
instructions and can also be constructed imperatively.
class NodePart implements Part {
readonly root?: PartRoot;
readonly metadata: string[];
readonly node: Node;
constructor(node: Node, init: { metadata?: string[] } = {}) {}
disconnect(): void;
}
A ChildNodePart
is constructed for <?child-node-part?>
instructions and can also be constructed imperatively.
class ChildNodePart implements Part, PartRoot {
readonly root?: PartRoot;
readonly metadata: string[];
readonly previousSibling: Node;
readonly nextSibling: Node;
constructor(
previousSibling: Node,
nextSibling: Node,
init: { metadata?: string[] } = {}
) {}
children(): Node[] {}
// All parts in this subtree.
getParts(): Part[] {}
// Replaces the children and parts in this range.
replaceChildren(...nodes: Array<Node | string>) {}
disconnect(): void;
}
ChildNodePart
is constructed with previousSibling
and nextSibling
nodes. The validity of the ChildNodePart
is determined from those nodes - they must be ordered, contiguous, and non-overlapping with any other ChildNodePart
objects.
Invalid ChildNodePart
objects are still accessible in with getParts()
, but never have children.
Unlike NodePart
, ChildNodePart
is also a PartRoot
like a Document
or DocumentFragment
. This means that it can contain content and nodes, and can be a PartRoot
for other parts.
Processing instructions have some drawbacks as well. The major drawback is that they are not commonly output by HTML generating libraries, and so it may be a challenge for adoption in those libraries.
Additionally processing instructions cannot be output inside tags, so possible extensions like attribute parts are more difficult to express.
Alternatives to processing instructions considered:
- Comments with specific structure. These could be used in place of processing instructions but are not valid inside tags.
- A new special character, for example
{}
that could be specially parsed in a document mode. This comes with all the drawbacks of complexity for a new document mode.