Skip to content

Commit aeec70f

Browse files
noamrchromium-wpt-export-bot
authored andcommitted
Support <template for="..." buffer>
The "buffer" boolean attribute makes it so that the template content streams into the template, and only applies to the target as a patch once the template is done. During streaming, the content is available in template.content. (behind the DeclalrativeFragment flag) Bug: 535664974 Change-Id: I2ca998553e26725bb8e488f08efe2e196b2d080a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8124335 Reviewed-by: Mason Freed <masonf@chromium.org> Commit-Queue: Noam Rosenthal <nrosenthal@google.com> Cr-Commit-Position: refs/heads/main@{#1668718}
1 parent 177f223 commit aeec70f

8 files changed

Lines changed: 341 additions & 0 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: buffer and for immutability after start tag</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<div id="container">
8+
<span>Before</span>
9+
<div id="target-original" marker="original-marker">
10+
<?start name="original-marker">Original Content<?end>
11+
</div>
12+
<div id="target-new" marker="new-marker">
13+
<?start name="new-marker">New Content<?end>
14+
</div>
15+
16+
<!-- We start with a targeted buffered template -->
17+
<template for="original-marker" buffer id="test-template">
18+
<span id="target1">Inside 1</span>
19+
<script>
20+
// Dynamically change/remove attributes during parsing
21+
const tpl = document.getElementById('test-template');
22+
tpl.setAttribute('for', 'new-marker'); // Change target
23+
tpl.removeAttribute('buffer'); // Disable buffering (switch to streaming)
24+
</script>
25+
<span id="target2">Inside 2</span>
26+
</template>
27+
<span>After</span>
28+
</div>
29+
<script>
30+
test(() => {
31+
const container = document.getElementById('container');
32+
const targetOriginal = document.getElementById('target-original');
33+
const targetNew = document.getElementById('target-new');
34+
35+
// Verify that the changes had NO EFFECT:
36+
// 1. Buffering was still active, so no streaming occurred during parsing.
37+
// 2. The target was still "original-marker" (so targetOriginal is replaced).
38+
assert_equals(targetOriginal.querySelector('#target1').textContent, 'Inside 1');
39+
assert_equals(targetOriginal.querySelector('#target2').textContent, 'Inside 2');
40+
41+
// 3. targetNew remained untouched.
42+
assert_equals(targetNew.textContent.trim().replace(/\s+/g, ' '), 'New Content');
43+
44+
// 4. The template is removed.
45+
assert_equals(container.querySelector('template'), null);
46+
}, "Modifying 'for' or 'buffer' attributes dynamically after start tag has no effect on the inclusion behavior");
47+
</script>
48+
</body>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: buffer attribute (in-place include)</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<div id="container">
8+
<span>Before</span>
9+
<template for buffer>
10+
<span id="target1">Inside 1</span>
11+
<script>
12+
window.target1PresentDuringScript = !!document.getElementById('target1');
13+
window.target2PresentDuringScript = !!document.getElementById('target2');
14+
</script>
15+
<span id="target2">Inside 2</span>
16+
</template>
17+
<span>After</span>
18+
</div>
19+
<script>
20+
test(() => {
21+
const container = document.getElementById('container');
22+
// Verify children insertion and order by checking adjacent elements
23+
assert_equals(container.querySelector('#target1').previousElementSibling.textContent, 'Before');
24+
assert_equals(container.querySelector('#target2').nextElementSibling.textContent, 'After');
25+
26+
// Verify that the template is not in the DOM
27+
assert_equals(container.querySelector('template'), null);
28+
29+
// Verify that the script executed after all children were parsed (atomic insert)
30+
assert_true(window.target1PresentDuringScript, "target1 should be present during script execution");
31+
assert_true(window.target2PresentDuringScript, "target2 should be present during script execution (atomic buffering check)");
32+
}, "In-place template with buffer attribute buffers children and inserts them atomically at finalization");
33+
</script>
34+
</body>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: MutationObserver atomic insertion verification</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<div id="container">
8+
<span>Before</span>
9+
<div id="target" marker="dest-marker">
10+
<?start name="dest-marker">Original Content<?end>
11+
</div>
12+
13+
<script>
14+
window.callbackCount = 0;
15+
window.totalAddedElements = 0;
16+
17+
const target = document.getElementById('target');
18+
const observer = new MutationObserver((mutationsList) => {
19+
window.callbackCount++;
20+
for (const mutation of mutationsList) {
21+
if (mutation.type === 'childList') {
22+
for (const node of mutation.addedNodes) {
23+
if (node.nodeType === Node.ELEMENT_NODE) {
24+
window.totalAddedElements++;
25+
}
26+
}
27+
}
28+
}
29+
});
30+
observer.observe(target, { childList: true, subtree: true });
31+
</script>
32+
33+
<template for="dest-marker" buffer>
34+
<span id="child1">Inside 1</span>
35+
<span id="child2">Inside 2</span>
36+
<span id="child3">Inside 3</span>
37+
</template>
38+
<span>After</span>
39+
</div>
40+
<script>
41+
// We wait for a microtask cycle to ensure the MutationObserver has run its callback
42+
promise_test(async () => {
43+
await new Promise(resolve => step_timeout(resolve, 0));
44+
45+
const target = document.getElementById('target');
46+
47+
// Verify that the children were inserted
48+
assert_equals(target.querySelector('#child1').textContent, 'Inside 1');
49+
assert_equals(target.querySelector('#child2').textContent, 'Inside 2');
50+
assert_equals(target.querySelector('#child3').textContent, 'Inside 3');
51+
52+
// Verify MutationObserver stats:
53+
// - The observer callback should have been invoked exactly once (atomic microtask delivery)
54+
assert_equals(window.callbackCount, 1, "The MutationObserver callback should be invoked exactly once");
55+
assert_equals(window.totalAddedElements, 3, "Total added elements across mutations should be 3");
56+
}, "Targeted template with buffer attribute triggers atomic MutationObserver records");
57+
</script>
58+
</body>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: buffer attribute reflection</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<script>
8+
test(() => {
9+
const template = document.createElement('template');
10+
11+
// Verify default value is false
12+
assert_equals(template.buffer, false, "default buffer value should be false");
13+
assert_false(template.hasAttribute('buffer'), "default state should not have buffer attribute");
14+
15+
// Verify setting property to true sets attribute
16+
template.buffer = true;
17+
assert_equals(template.buffer, true, "setting buffer to true should update property");
18+
assert_true(template.hasAttribute('buffer'), "setting buffer to true should add buffer attribute");
19+
assert_equals(template.getAttribute('buffer'), '', "buffer attribute should be empty string (boolean attribute)");
20+
21+
// Verify setting property to false removes attribute
22+
template.buffer = false;
23+
assert_equals(template.buffer, false, "setting buffer to false should update property");
24+
assert_false(template.hasAttribute('buffer'), "setting buffer to false should remove buffer attribute");
25+
26+
// Verify setting attribute updates property
27+
template.setAttribute('buffer', '');
28+
assert_equals(template.buffer, true, "setting buffer attribute should update property to true");
29+
30+
template.removeAttribute('buffer');
31+
assert_equals(template.buffer, false, "removing buffer attribute should update property to false");
32+
}, "HTMLTemplateElement.buffer IDL attribute reflects the buffer content attribute");
33+
</script>
34+
</body>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: template.content reflects progressive streaming content</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<script>
8+
const t = async_test("template.content reflects the buffered content progressively while streaming");
9+
10+
window.onmessage = (event) => {
11+
if (event.data && event.data.type === 'test-result') {
12+
t.step(() => {
13+
assert_true(event.data.passed, event.data.message);
14+
});
15+
t.done();
16+
}
17+
};
18+
</script>
19+
<iframe src="buffer-streaming-reflection.py" style="width:0; height:0; border:0;"></iframe>
20+
</body>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import time
2+
3+
4+
def main(request, response):
5+
response.headers.set(b"Content-Type", b"text/html")
6+
response.status = 200
7+
response.write_status_headers()
8+
9+
# Chunk 1: Setup target and start template with first child, then start interval observer
10+
response.writer.write_content(b"""<!DOCTYPE html>
11+
<meta charset="utf-8">
12+
<body>
13+
<div id="target" marker="dest-marker">
14+
<?start name="dest-marker">Original Content<?end>
15+
</div>
16+
17+
<script>
18+
window.observedLengths = [];
19+
const intervalId = setInterval(() => {
20+
const tpl = document.getElementById('test-template');
21+
if (tpl) {
22+
const elementNodes = Array.from(tpl.content.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE);
23+
window.observedLengths.push(elementNodes.length);
24+
}
25+
}, 10);
26+
</script>
27+
28+
<template for="dest-marker" buffer id="test-template">
29+
<span id="child1">One</span>
30+
""")
31+
32+
# Yield and wait to allow the interval to fire while the template is still open
33+
time.sleep(0.5)
34+
35+
# Chunk 2: Append second child, close template, clear interval and post result
36+
response.writer.write_content(b""" <span id="child2">Two</span>
37+
</template>
38+
39+
<script>
40+
clearInterval(intervalId);
41+
42+
// Wait a microtask to ensure finalization completed
43+
setTimeout(() => {
44+
const target = document.getElementById('target');
45+
const child1_ok = target.querySelector('#child1') && target.querySelector('#child1').textContent === 'One';
46+
const child2_ok = target.querySelector('#child2') && target.querySelector('#child2').textContent === 'Two';
47+
const tpl_removed = document.getElementById('test-template') === null;
48+
const progress_ok = window.observedLengths.includes(1);
49+
50+
let passed = child1_ok && child2_ok && tpl_removed && progress_ok;
51+
let message = "";
52+
if (!passed) {
53+
message = `child1: ${child1_ok}, child2: ${child2_ok}, tpl_removed: ${tpl_removed}, progress_ok (lengths includes 1): ${progress_ok}. Observed: ${JSON.stringify(window.observedLengths)}`;
54+
}
55+
56+
window.parent.postMessage({
57+
type: 'test-result',
58+
passed: passed,
59+
message: message
60+
}, "*");
61+
}, 0);
62+
</script>
63+
</body>
64+
""")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: buffer attribute (targeted include)</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<div id="container">
8+
<span>Before</span>
9+
<div id="target" marker="dest-marker">
10+
<?start name="dest-marker">Original Content<?end>
11+
</div>
12+
<template for="dest-marker" buffer>
13+
<span id="target1">Inside 1</span>
14+
<script>
15+
window.target1PresentTargeted = !!document.getElementById('target1');
16+
window.target2PresentTargeted = !!document.getElementById('target2');
17+
</script>
18+
<span id="target2">Inside 2</span>
19+
</template>
20+
<span>After</span>
21+
</div>
22+
<script>
23+
test(() => {
24+
const container = document.getElementById('container');
25+
const target = document.getElementById('target');
26+
27+
// Verify target content replacement
28+
assert_equals(target.querySelector('#target1').textContent, 'Inside 1');
29+
assert_equals(target.querySelector('#target2').textContent, 'Inside 2');
30+
31+
// Verify that the template is not in the DOM
32+
assert_equals(container.querySelector('template'), null);
33+
34+
// Verify that the script executed after all children were parsed (atomic insert)
35+
assert_true(window.target1PresentTargeted, "target1 should be present during script execution");
36+
assert_true(window.target2PresentTargeted, "target2 should be present during script execution (atomic buffering check)");
37+
}, "Targeted template with buffer attribute buffers children and replaces target content atomically at finalization");
38+
</script>
39+
</body>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Declarative Fragment: streaming target marker removal during parsing</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<body>
7+
<div id="target" marker="dest-marker">
8+
<?start name="dest-marker">Original Content<?end>
9+
</div>
10+
11+
<template for="dest-marker">
12+
<span id="child1">One</span>
13+
<script>
14+
// Remove the target <?end> PI while streaming is mid-way
15+
const target = document.getElementById('target');
16+
for (const child of Array.from(target.childNodes)) {
17+
if (child.nodeType === Node.PROCESSING_INSTRUCTION_NODE && child.target === 'end') {
18+
child.remove();
19+
}
20+
}
21+
</script>
22+
<span id="child2">Two</span>
23+
</template>
24+
25+
<script>
26+
test(() => {
27+
const target = document.getElementById('target');
28+
29+
// Verify that both elements were successfully streamed into target,
30+
// falling back to appending when the end marker was removed.
31+
assert_equals(target.querySelector('#child1').textContent, 'One');
32+
assert_equals(target.querySelector('#child2').textContent, 'Two');
33+
34+
// Verify order: child1 -> child2 (ignoring script elements)
35+
const children = Array.from(target.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE && n.tagName.toLowerCase() !== 'script');
36+
assert_equals(children.length, 2, "There should be exactly 2 non-script element children");
37+
assert_equals(children[0].id, 'child1');
38+
assert_equals(children[1].id, 'child2');
39+
40+
// Verify that the template is not in the DOM
41+
assert_equals(target.querySelector('template'), null);
42+
}, "Streaming target end marker can be removed mid-stream safely, falling back to appending");
43+
</script>
44+
</body>

0 commit comments

Comments
 (0)