Skip to content

Commit e1a05ff

Browse files
Extract document-fragment support from shadow DOM PR
Agent-Logs-Url: https://github.com/emberjs/ember.js/sessions/b223b6e1-6551-4db1-bb11-99dac1313bec Co-authored-by: NullVoxPopuli <[email protected]>
1 parent 0531e62 commit e1a05ff

4 files changed

Lines changed: 174 additions & 1 deletion

File tree

packages/@glimmer-workspace/integration-tests/lib/suites.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './suites/entry-point';
77
export * from './suites/has-block';
88
export * from './suites/has-block-params';
99
export * from './suites/in-element';
10+
export * from './suites/in-element-document-fragment';
1011
export * from './suites/initial-render';
1112
export * from './suites/scope';
1213
export * from './suites/shadowing';
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { RenderTest } from '../render-test';
2+
import { test } from '../test-decorator';
3+
4+
export class InElementDocumentFragmentSuite extends RenderTest {
5+
static suiteName = '#in-element (DocumentFragment)';
6+
7+
@test
8+
'Renders curlies into a detached DocumentFragment'() {
9+
const fragment = document.createDocumentFragment();
10+
11+
this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', {
12+
fragment,
13+
foo: 'Hello Fragment!',
14+
});
15+
16+
this.assert.strictEqual(
17+
fragment.textContent,
18+
'[Hello Fragment!]',
19+
'content rendered in document fragment'
20+
);
21+
this.assertHTML('<!---->');
22+
this.assertStableRerender();
23+
24+
this.rerender({ foo: 'Updated!' });
25+
this.assert.strictEqual(
26+
fragment.textContent,
27+
'[Updated!]',
28+
'content updated in document fragment'
29+
);
30+
this.assertHTML('<!---->');
31+
32+
this.rerender({ foo: 'Hello Fragment!' });
33+
this.assert.strictEqual(
34+
fragment.textContent,
35+
'[Hello Fragment!]',
36+
'content reverted in document fragment'
37+
);
38+
this.assertHTML('<!---->');
39+
}
40+
41+
@test
42+
'Renders curlies into a template.content fragment'() {
43+
const templateEl = document.createElement('template');
44+
const fragment = templateEl.content;
45+
46+
this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', {
47+
fragment,
48+
foo: 'Hello Template Content!',
49+
});
50+
51+
this.assert.strictEqual(
52+
fragment.textContent,
53+
'[Hello Template Content!]',
54+
'content rendered in template.content fragment'
55+
);
56+
this.assertHTML('<!---->');
57+
this.assertStableRerender();
58+
59+
this.rerender({ foo: 'Updated!' });
60+
this.assert.strictEqual(
61+
fragment.textContent,
62+
'[Updated!]',
63+
'content updated in template.content fragment'
64+
);
65+
this.assertHTML('<!---->');
66+
67+
this.rerender({ foo: 'Hello Template Content!' });
68+
this.assert.strictEqual(
69+
fragment.textContent,
70+
'[Hello Template Content!]',
71+
'content reverted in template.content fragment'
72+
);
73+
this.assertHTML('<!---->');
74+
}
75+
76+
@test
77+
'Renders elements into a fragment that is later attached to the DOM'() {
78+
const fragment = document.createDocumentFragment();
79+
const container = document.createElement('div');
80+
81+
this.render('{{#in-element this.fragment}}<p id="frag-p">{{this.message}}</p>{{/in-element}}', {
82+
fragment,
83+
message: 'in fragment',
84+
});
85+
86+
this.assert.strictEqual(
87+
fragment.querySelector('#frag-p')?.textContent,
88+
'in fragment',
89+
'content rendered in detached fragment'
90+
);
91+
this.assertHTML('<!---->');
92+
93+
// Attach fragment's children to the DOM
94+
container.appendChild(fragment);
95+
this.assert.strictEqual(
96+
container.querySelector('#frag-p')?.textContent,
97+
'in fragment',
98+
'content is in the DOM after fragment is appended'
99+
);
100+
// Fragment itself is now empty (children moved to container)
101+
this.assert.strictEqual(fragment.childNodes.length, 0, 'fragment is empty after append');
102+
}
103+
104+
@test
105+
'Multiple in-element calls to the same DocumentFragment'() {
106+
const fragment = document.createDocumentFragment();
107+
108+
this.render(
109+
'{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}' +
110+
'{{#in-element this.fragment insertBefore=null}}[{{this.bar}}]{{/in-element}}',
111+
{
112+
fragment,
113+
foo: 'first',
114+
bar: 'second',
115+
}
116+
);
117+
118+
this.assert.ok(fragment.textContent?.includes('[first]'), 'first block present in fragment');
119+
this.assert.ok(fragment.textContent?.includes('[second]'), 'second block present in fragment');
120+
this.assertHTML('<!----><!---->');
121+
this.assertStableRerender();
122+
123+
this.rerender({ foo: 'updated-first', bar: 'updated-second' });
124+
this.assert.ok(
125+
fragment.textContent?.includes('[updated-first]'),
126+
'first block updated in fragment'
127+
);
128+
this.assert.ok(
129+
fragment.textContent?.includes('[updated-second]'),
130+
'second block updated in fragment'
131+
);
132+
this.assertHTML('<!----><!---->');
133+
}
134+
135+
@test
136+
'Multiple in-element calls to the same DocumentFragment with insertBefore=null'() {
137+
const fragment = document.createDocumentFragment();
138+
139+
this.render(
140+
'{{#in-element this.fragment insertBefore=null}}<p id="a">{{this.foo}}</p>{{/in-element}}' +
141+
'{{#in-element this.fragment insertBefore=null}}<p id="b">{{this.bar}}</p>{{/in-element}}',
142+
{
143+
fragment,
144+
foo: 'first',
145+
bar: 'second',
146+
}
147+
);
148+
149+
// Use childNodes to query into the fragment since querySelector doesn't work on detached fragment nodes in all browsers
150+
const nodes = Array.from(fragment.childNodes);
151+
const pA = nodes.find((n) => (n as Element).id === 'a') as HTMLElement | undefined;
152+
const pB = nodes.find((n) => (n as Element).id === 'b') as HTMLElement | undefined;
153+
154+
this.assert.strictEqual(pA?.textContent, 'first', 'first block appended to fragment');
155+
this.assert.strictEqual(pB?.textContent, 'second', 'second block appended to fragment');
156+
this.assertHTML('<!----><!---->');
157+
this.assertStableRerender();
158+
159+
this.rerender({ foo: 'updated-first', bar: 'updated-second' });
160+
this.assert.strictEqual(pA?.textContent, 'updated-first', 'first block updated in fragment');
161+
this.assert.strictEqual(pB?.textContent, 'updated-second', 'second block updated in fragment');
162+
this.assertHTML('<!----><!---->');
163+
}
164+
}

packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
GlimmerishComponents,
66
HasBlockParamsHelperSuite,
77
HasBlockSuite,
8+
InElementDocumentFragmentSuite,
89
InElementSuite,
910
jitComponentSuite,
1011
jitSuite,
@@ -18,6 +19,7 @@ import {
1819
jitComponentSuite(DebuggerSuite);
1920
jitSuite(EachSuite);
2021
jitSuite(InElementSuite);
22+
jitSuite(InElementDocumentFragmentSuite);
2123

2224
jitComponentSuite(GlimmerishComponents);
2325
jitComponentSuite(TemplateOnlyComponents);

packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ModifierInstance,
88
Nullable,
99
Owner,
10+
SimpleElement,
1011
UpdatingOpcode,
1112
UpdatingVM,
1213
} from '@glimmer/interfaces';
@@ -29,10 +30,12 @@ import {
2930
} from '@glimmer/constants';
3031
import {
3132
check,
33+
CheckDocumentFragment,
3234
CheckElement,
3335
CheckMaybe,
3436
CheckNode,
3537
CheckNullable,
38+
CheckOr,
3639
CheckString,
3740
} from '@glimmer/debug';
3841
import { debugToString, expect } from '@glimmer/debug-util';
@@ -74,7 +77,10 @@ APPEND_OPCODES.add(VM_PUSH_REMOTE_ELEMENT_OP, (vm) => {
7477
let insertBeforeRef = check(vm.stack.pop(), CheckReference);
7578
let guidRef = check(vm.stack.pop(), CheckReference);
7679

77-
let element = check(valueForRef(elementRef), CheckElement);
80+
let element = check(
81+
valueForRef(elementRef),
82+
CheckOr(CheckElement, CheckDocumentFragment)
83+
) as SimpleElement;
7884
let insertBefore = check(valueForRef(insertBeforeRef), CheckMaybe(CheckNullable(CheckNode)));
7985
let guid = valueForRef(guidRef) as string;
8086

0 commit comments

Comments
 (0)