Skip to content

Commit 1101fbf

Browse files
authored
Overhaul the patching explainer (#74)
1 parent 555ac49 commit 1101fbf

File tree

1 file changed

+192
-59
lines changed

1 file changed

+192
-59
lines changed

patching-explainer.md

Lines changed: 192 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,78 +13,210 @@ For example, React streams content out of order by injecting inline `<script>` t
1313

1414
This proposal introduces partial out-of-order HTML streaming as part of the web platform.
1515

16-
## Patching
17-
A "patch" is a stream of HTML content, that can be injected into an existing position in the DOM.
18-
A patch can be streamed directly into that position using JavaScript, and multiple patches can be interleaved in an HTML document, allowing for out-of-order content as part of an ordinary HTML response.
16+
## Declarative patching
1917

20-
## Anatomy of a patch
18+
Patches are delivered using a `<template>` element with the `contentmethod` attribute and target an existing elements in the DOM with the `contentname` attributes. These patches require no scripts to apply (are declarative) and can appear in the main response HTML to support out-of-order streaming.
2119

22-
A patch is a [stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) that targets a [parent node](https://developer.mozilla.org/en-US/docs/Web/API/Node) (usually an element, but potentially a shadow root).
23-
It can handle strings, bytes, or `TrustedHTMLString`. When it receives bytes, it decodes them using UTF8.
24-
Anything other than strings or bytes is stringified.
20+
Patches can be be applied later in the page lifecycle using JavaScript, see [script-initiated patching](#script-initiated-patching).
2521

26-
When a patch is active, it is essentially a [WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) that feeds a [fragment-mode parser](https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm) with strings from that stream.
27-
Unlike the usual fragment parser, nodes are inserted directly into the target and not buffered into the fragment first. The fragment parser is only used to set up the parser context.
28-
It is similar to calling `document.write()`, scoped to an node.
22+
### Proposed markup
2923

30-
## One-off patching
24+
The `contentname` attribute is used to identify an element which can be patched:
3125

32-
The most atomic form of patching is opening a container node for writing, creating a `WritableStream` for it.
33-
This can be done with an API as such:
34-
```js
35-
const writable = elementOrShadowRoot.streamHTMLUnsafe({runScripts: true});
36-
byteOrTextStream.pipeTo(writable);
26+
```html
27+
<section contentname=gallery>Loading...</section>
3728
```
3829

39-
A few details about one-off patching:
40-
- Streams do not abort each other. It is the author's responsibility to manage conflicts between multiple streams.
41-
- Unlike contextual fragments, when `runScripts` is true, classic scripts in the stream can block the parser until they are fetched. This makes the streaming parser behave more similarly to the main parser.
42-
- Only the unsafe variant can run scripts.
43-
- This describes `streamHTML`, but also `streamAppendHTML`, `streamPrependHTML`, `streamBeforeHTML`, `streamAfterHTML`, and `streamReplaceWithHTML` variants are proposed.
30+
The content is then patches using a `<template>` element:
4431

45-
To account for HTML sanitation, this API would have an "Unsafe" version and would accept a sanitizer in its option, like [`setHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML):
46-
```js
47-
byteOrTextStream.pipeTo(elementOrShadowRoot.streamHTML({sanitizer}));
48-
byteOrTextStream.pipeTo(elementOrShadowRoot.streamHTMLUnsafe({sanitizer, runScripts}));
32+
```html
33+
<template contentmethod="replace-children">
34+
<section contentname=gallery>Actual gallery content<section>
35+
</template>
4936
```
5037

51-
Since user-space sanitizers like DOMPurify are not well suited for streaming, TrustedTypes only allows streaming with either sanitation or by giving it a "free pass", by blessing parser options:
52-
```js
53-
// This would fail if there is a default policy with `createHTML`
54-
element.streamHTMLUnsafe({sanitizer, runScripts});
38+
The element name (`section`) needs to be repeated so that children are parsed correctly, but only the child nodes are actually replaced in this example.
39+
40+
There are two proposed `contentmethod` values:
41+
42+
- `append` inserts nodes at the end of the element, similar to `element.append(nodes)`.
43+
- `replace-children` replaces any existing child nodes, similar to `element.replaceChildren(nodes)`.
44+
45+
At a low level, the only difference is that is `replace-children` removes existing nodes and then appends new nodes, while `append` only appends new nodes.
46+
47+
A few details about interleaved patching:
48+
- Templates with a valid `contentmethod` are not attached to the DOM.
49+
- If the patching element is not a direct child of `<body>`, the outlet has to have a common ancestor with the patching element's parent.
50+
- The patch template has to be in the same tree (shadow) scope as the outlet.
51+
52+
See the https://github.com/whatwg/html/pull/11818 for the full processing model and details.
53+
54+
### Interleaved patching
55+
56+
An element can be patched multiple times and patches for different elements can be interleaved. This allows for updates to different parts of the document to be interleaved. For example:
5557

56-
// This would "bless" the parser options for streaming.
57-
element.streamHTMLUnsafe(trustedSourcePolicy.createParserOptions({sanitizer, runScripts});
58+
```html
59+
<div contentname=product-carousel>Loading...</div>
60+
<div contentname=search-results>Loading...</div>
5861
```
5962

60-
Also see detailed discussion at https://github.com/whatwg/html/issues/11669.
63+
In this example, the search results populate in three steps while the product carousel populates in one step in between:
6164

62-
## Interleaved patching
65+
```html
66+
<template contentmethod=replace-children>
67+
<div contentname=search-results>
68+
<p>first result</p>
69+
</div>
70+
</template>
6371

64-
In addition to invoking streaming using script, this proposal includes patching interleaved inside HTML content. A `<template>` would have a special attribute that
65-
parses its content as raw text, finds the target element using attributes, and reroutes the raw text content to the target element:
72+
<template contentmethod=replace-children>
73+
<div contentname=product-carousel>Actual carousel content</div>
74+
</template>
75+
76+
<template contentmethod=append>
77+
<div contentname=search-results>
78+
<p>second result</p>
79+
</div>
80+
</template>
81+
82+
<template contentmethod=append>
83+
<div contentname=search-results>
84+
<p>third result</p>
85+
</div>
86+
</template>
87+
```
88+
89+
#### Alternatives considered
90+
91+
A few variations to support interleaved patching have been considered:
92+
93+
##### Automatic defaults
94+
95+
To remove children the first time an element is targeted, and to append if it is targeted again within the same parser invocation. In this alternative, the opt-in to patching would be a boolean attribute like `contentupdate` on `<template>`, and `contentmethod` is only used to override the default.
96+
97+
<details>
98+
<summary>Example</summary>
99+
100+
The patches use `contentupdate` instead of `contentmethod`:
66101

67102
```html
68-
<section contentname=gallery>Loading...</section>
103+
<template contentupdate>
104+
<div contentname=search-results>
105+
</div>
106+
</template>
69107

70-
<!-- later -->
71-
<template contentmethod="replace-children"><section contentname=gallery>Actual gallery content<section></template>
108+
<template contentupdate>
109+
<div contentname=product-carousel>Actual carousel content</div>
110+
</template>
111+
112+
<template contentupdate>
113+
<div contentname=search-results>
114+
<p>second result</p>
115+
</div>
116+
</template>
117+
118+
<template contentupdate>
119+
<div contentname=search-results>
120+
<p>third result</p>
121+
</div>
122+
</template>
72123
```
73124

74-
A few details about interleaved patching:
75-
- Templates with a valid `contentmethod` are not attached to the DOM.
76-
- If the patching element is not a direct child of `<body>`, the outlet has to have a common ancestor with the patching element's parent.
77-
- The patch template has to be in the same tree (shadow) scope as the outlet.
78-
- `contentmethod` can be `replace-children`, or `append`. `replace-children` is the basic one that allows replacing a placeholder with its contents,
79-
while `append` allows for multiple patches that are interleaved in the same HTML stream to accumulate.
80-
- Interleaved patching works together with one-off patching. When a `<template contentmethod>` appears inside a stream, it is applied, resolving `contentname` from the stream target.
125+
</details>
126+
127+
(For an append-only use case, `contentmethod` would still be needed in addition to `contentupdate`.)
128+
129+
##### Range markers
130+
131+
Don't support `contentmethod=append` and instead support this use case using [markers](#streaming-to-non-element-ranges). To "append", target two markers with no content between them are used. For multiple appends, each patch would need to insert an additional marker for the next patch to target.
132+
133+
<details>
134+
<summary>Example</summary>
135+
136+
The patches uses two markers to "emulate" append:
137+
138+
```html
139+
<template contentmethod=replace-children>
140+
<div contentname=search-results>
141+
<p>first result</p>
142+
<!-- add markers to allow for "append" -->
143+
<?marker name=m1?><?marker name=m2?>
144+
</div>
145+
</template>
146+
147+
<template contentmethod=replace-children>
148+
<div contentname=product-carousel>Actual carousel content</div>
149+
</template>
150+
151+
<template contentmethod=replace-children contentmarkerstart=m1 contentmarkerend=m2>
152+
<div contentname=search-results>
153+
<p>second result</p>
154+
<!-- new markers are needed for the next "append". -->
155+
<?marker name=m3?><?marker name=m4?>
156+
</div>
157+
</template>
158+
159+
<template contentmethod=replace-children contentmarkerstart=m3 contentmarkerend=m4>
160+
<div contentname=search-results>
161+
<p>third result</p>
162+
</div>
163+
</template>
164+
```
165+
166+
</details>
167+
168+
##### Single marker
81169

82-
## Avoiding overwriting with identical content
170+
Similar to above, but instead of the `contentmarkerstart` and `contentmarkerend` attributes, a single marker node and the `contentmarkerstartbefore` attribute is used to define a range starting before the node and implicitly ending at the end of the container element. For multiple appends, each patch would need to insert a new marker at the end, but it could have the same name as the replaced marker.
171+
172+
<details>
173+
<summary>Example</summary>
174+
175+
The patches uses a single marker node:
176+
177+
```html
178+
<template contentmethod=replace-children>
179+
<div contentname=search-results>
180+
<p>first result</p>
181+
<!-- add markers to allow for "append" -->
182+
<?marker name=more?>
183+
</div>
184+
</template>
185+
186+
<template contentmethod=replace-children>
187+
<div contentname=product-carousel>Actual carousel content</div>
188+
</template>
189+
190+
<template contentmethod=replace-children contentmarkerstartbefore=more>
191+
<div contentname=search-results>
192+
<p>second result</p>
193+
<!-- new markers are needed for the next "append". -->
194+
<?marker name=more?>
195+
</div>
196+
</template>
197+
198+
<template contentmethod=replace-children contentmarkerstartbefore=more>
199+
<div contentname=search-results>
200+
<p>third result</p>
201+
</div>
202+
</template>
203+
```
204+
205+
</details>
206+
207+
## Script-initiated patching
208+
209+
`streamHTMLUnsafe()` is being pursued as a [separate proposal](https://github.com/whatwg/html/issues/2142), but will also work with patching. When `<template contentmethod>` appears in the streamed HTML, those patches can apply to descendants of element on which `streamHTMLUnsafe()` was called.
210+
211+
## Potential enhancement
212+
213+
### Avoiding overwriting with identical content
83214

84215
Some content might need to remain unchanged in certain conditions. For example, displaying a chat widget in all pages but the home, but not reloading it between pages.
85216
For this, both the outlet and the patch can have a `contentrevision` attribute. If those match, the content is not applied.
86217

87-
## Potential enhancement - streaming to non-element ranges
218+
### Streaming to non-element ranges
219+
88220
See discussion in https://github.com/WICG/declarative-partial-updates/issues/6 and https://github.com/WICG/webcomponents/issues/1116.
89221

90222
It has been a common request to stream not just by replacing the whole contents of an element or appending to it, but also by replacing an arbitrary range.
@@ -96,31 +228,32 @@ To achieve these use cases, the direction is to use addressable comments as per
96228
Very initial example:
97229

98230
```html
99-
<table contentname="data">
100-
<tr><td>static data
101-
<tr><td>static data
102-
103-
<?marker name=dyn-start?>
104-
<tr><td>dynamic data 1
105-
<tr><td>dynamic data 2
106-
<?marker name=dyn-end?>
231+
<table>
232+
<tbody contentname=data>
233+
<tr><td>static data</td></tr>
234+
<tr><td>static data</td></tr>
235+
236+
<?marker name=dyn-start?>
237+
<tr><td>dynamic data 1</td></tr>
238+
<tr><td>dynamic data 2</td></tr>
239+
<?marker name=dyn-end?>
240+
</tbody>
107241
</table>
108242

109243
<!-- stuff.... -->
110244

111245
<!-- This would replace the children only between the dyn-start and dyn-end markers, leaving the static data alone. -->
112246
<template contentmethod="replace-children" contentmarkerstart="dyn-start" contentmarkerend="dyn-end">
113-
<table contentname=data>
247+
<tbody contentname=data>
114248
<tr><td>dynamic data 3
115249
<tr><td>dynamic data 4
116250
<tr><td>dynamic data 5
117-
</table>
251+
</tbody>
118252
</template>
119253
</body>
120254
```
121255

122-
123-
## Potential enhancement - patch contents from URL
256+
### Patch contents from URL
124257

125258
In addition to patching from a stream or interleaved in HTML, there are use-cases for patching by fetching a URL.
126259
This can be done with a `patchsrc` attribute.

0 commit comments

Comments
 (0)