diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts
index cfc74e0e423..7bd2cef99e8 100644
--- a/packages/@ember/-internals/glimmer/lib/renderer.ts
+++ b/packages/@ember/-internals/glimmer/lib/renderer.ts
@@ -991,7 +991,7 @@ export class Renderer extends BaseRenderer {
}
getBounds(component: View): {
- parentElement: SimpleElement;
+ parentElement: SimpleNode;
firstNode: SimpleNode;
lastNode: SimpleNode;
} {
diff --git a/packages/@glimmer-workspace/integration-tests/lib/dom/simple-utils.ts b/packages/@glimmer-workspace/integration-tests/lib/dom/simple-utils.ts
index 0823f5e3302..389565225b6 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/dom/simple-utils.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/dom/simple-utils.ts
@@ -21,12 +21,12 @@ import { clearElement } from '@glimmer/util';
import Serializer from '@simple-dom/serializer';
import voidMap from '@simple-dom/void-map';
-export function toInnerHTML(parent: SimpleElement | SimpleDocumentFragment): string {
+export function toInnerHTML(parent: SimpleNode): string {
const serializer = new Serializer(voidMap);
return serializer.serializeChildren(parent);
}
-export function toOuterHTML(parent: SimpleElement | SimpleDocumentFragment): string {
+export function toOuterHTML(parent: SimpleNode): string {
const serializer = new Serializer(voidMap);
return serializer.serialize(parent);
}
diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites.ts b/packages/@glimmer-workspace/integration-tests/lib/suites.ts
index a3ecce2e83c..1526e13312d 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/suites.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/suites.ts
@@ -7,6 +7,7 @@ export * from './suites/entry-point';
export * from './suites/has-block';
export * from './suites/has-block-params';
export * from './suites/in-element';
+export * from './suites/in-element-document-fragment';
export * from './suites/initial-render';
export * from './suites/scope';
export * from './suites/shadowing';
diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts
new file mode 100644
index 00000000000..3a122f4f543
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts
@@ -0,0 +1,220 @@
+import { RenderTest } from '../render-test';
+import { test } from '../test-decorator';
+
+export class InElementDocumentFragmentSuite extends RenderTest {
+ static suiteName = '#in-element (DocumentFragment)';
+
+ @test
+ 'Renders curlies into a detached DocumentFragment'() {
+ const fragment = document.createDocumentFragment();
+
+ this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', {
+ fragment,
+ foo: 'Hello Fragment!',
+ });
+
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Hello Fragment!]',
+ 'content rendered in document fragment'
+ );
+ this.assertHTML('');
+ this.assertStableRerender();
+
+ this.rerender({ foo: 'Updated!' });
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Updated!]',
+ 'content updated in document fragment'
+ );
+ this.assertHTML('');
+
+ this.rerender({ foo: 'Hello Fragment!' });
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Hello Fragment!]',
+ 'content reverted in document fragment'
+ );
+ this.assertHTML('');
+ }
+
+ @test
+ 'Renders curlies into a template.content fragment'() {
+ const templateEl = document.createElement('template');
+ const fragment = templateEl.content;
+
+ this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', {
+ fragment,
+ foo: 'Hello Template Content!',
+ });
+
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Hello Template Content!]',
+ 'content rendered in template.content fragment'
+ );
+ this.assertHTML('');
+ this.assertStableRerender();
+
+ this.rerender({ foo: 'Updated!' });
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Updated!]',
+ 'content updated in template.content fragment'
+ );
+ this.assertHTML('');
+
+ this.rerender({ foo: 'Hello Template Content!' });
+ this.assert.strictEqual(
+ fragment.textContent,
+ '[Hello Template Content!]',
+ 'content reverted in template.content fragment'
+ );
+ this.assertHTML('');
+ }
+
+ @test
+ 'Renders elements into a fragment that is later attached to the DOM'() {
+ const fragment = document.createDocumentFragment();
+ const container = document.createElement('div');
+
+ this.render('{{#in-element this.fragment}}
{{this.message}}
{{/in-element}}', {
+ fragment,
+ message: 'in fragment',
+ });
+
+ this.assert.strictEqual(
+ fragment.querySelector('#frag-p')?.textContent,
+ 'in fragment',
+ 'content rendered in detached fragment'
+ );
+ this.assertHTML('');
+
+ // Attach fragment's children to the DOM
+ container.appendChild(fragment);
+ this.assert.strictEqual(
+ container.querySelector('#frag-p')?.textContent,
+ 'in fragment',
+ 'content is in the DOM after fragment is appended'
+ );
+ // Fragment itself is now empty (children moved to container)
+ this.assert.strictEqual(fragment.childNodes.length, 0, 'fragment is empty after append');
+ }
+
+ @test
+ 'Multiple in-element calls to the same DocumentFragment'() {
+ const fragment = document.createDocumentFragment();
+
+ this.render(
+ '{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}' +
+ '{{#in-element this.fragment insertBefore=null}}[{{this.bar}}]{{/in-element}}',
+ {
+ fragment,
+ foo: 'first',
+ bar: 'second',
+ }
+ );
+
+ this.assert.ok(fragment.textContent?.includes('[first]'), 'first block present in fragment');
+ this.assert.ok(fragment.textContent?.includes('[second]'), 'second block present in fragment');
+ this.assertHTML('');
+ this.assertStableRerender();
+
+ this.rerender({ foo: 'updated-first', bar: 'updated-second' });
+ this.assert.ok(
+ fragment.textContent?.includes('[updated-first]'),
+ 'first block updated in fragment'
+ );
+ this.assert.ok(
+ fragment.textContent?.includes('[updated-second]'),
+ 'second block updated in fragment'
+ );
+ this.assertHTML('');
+ }
+
+ @test
+ 'Rerenders work after DocumentFragment is appended to the DOM'(assert: typeof QUnit.assert) {
+ const fragment = document.createDocumentFragment();
+ const container = document.createElement('div');
+ const step = (text: string) => {
+ assert.step(text);
+ return text;
+ };
+
+ this.render(
+ '{{#in-element this.fragment}}' +
+ '{{this.step this.message}}
' +
+ '{{#if this.show}}' +
+ '' +
+ '{{/if}}' +
+ '{{/in-element}}',
+ {
+ fragment,
+ message: 'initial',
+ show: false,
+ step,
+ }
+ );
+
+ assert.verifySteps(['initial'], 'initial render fires step from inside fragment');
+
+ // Move the fragment's children into the container. After this the fragment is
+ // empty, but the rendered nodes (including Glimmer's bounds markers) are live
+ // children of `container`.
+ container.appendChild(fragment);
+ assert.strictEqual(fragment.childNodes.length, 0, 'fragment is empty after append');
+ assert.ok(container.querySelector('#msg'), 'paragraph is present in container after append');
+
+ // Rerenders should continue to work after the fragment is attached — Glimmer
+ // resolves the live parent from the bounds markers' actual parentNode.
+ this.rerender({ message: 'updated' });
+ assert.verifySteps(['updated'], 'text update fires step after fragment was attached to DOM');
+ assert.strictEqual(
+ container.querySelector('#msg')?.textContent,
+ 'updated',
+ 'paragraph text is updated in container'
+ );
+
+ // New conditional element should appear in the container.
+ this.rerender({ show: true });
+ assert.verifySteps(
+ ['extra rendered'],
+ 'conditional element step fires in container after fragment was attached to DOM'
+ );
+ assert.ok(
+ container.querySelector('#extra'),
+ 'conditional span appears in container after fragment was attached to DOM'
+ );
+ }
+
+ @test
+ 'Multiple in-element calls to the same DocumentFragment with insertBefore=null'() {
+ const fragment = document.createDocumentFragment();
+
+ this.render(
+ '{{#in-element this.fragment insertBefore=null}}{{this.foo}}
{{/in-element}}' +
+ '{{#in-element this.fragment insertBefore=null}}{{this.bar}}
{{/in-element}}',
+ {
+ fragment,
+ foo: 'first',
+ bar: 'second',
+ }
+ );
+
+ // Use childNodes to traverse the fragment's direct children since glimmer also
+ // inserts comment marker nodes alongside the rendered elements.
+ const nodes = Array.from(fragment.childNodes);
+ const pA = nodes.find((n) => (n as Element).id === 'a') as HTMLElement | undefined;
+ const pB = nodes.find((n) => (n as Element).id === 'b') as HTMLElement | undefined;
+
+ this.assert.strictEqual(pA?.textContent, 'first', 'first block appended to fragment');
+ this.assert.strictEqual(pB?.textContent, 'second', 'second block appended to fragment');
+ this.assertHTML('');
+ this.assertStableRerender();
+
+ this.rerender({ foo: 'updated-first', bar: 'updated-second' });
+ this.assert.strictEqual(pA?.textContent, 'updated-first', 'first block updated in fragment');
+ this.assert.strictEqual(pB?.textContent, 'updated-second', 'second block updated in fragment');
+ this.assertHTML('');
+ }
+}
diff --git a/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts b/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts
index 69afb5a202c..31d0c9a14ac 100644
--- a/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts
+++ b/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts
@@ -5,6 +5,7 @@ import {
GlimmerishComponents,
HasBlockParamsHelperSuite,
HasBlockSuite,
+ InElementDocumentFragmentSuite,
InElementSuite,
jitComponentSuite,
jitSuite,
@@ -18,6 +19,7 @@ import {
jitComponentSuite(DebuggerSuite);
jitSuite(EachSuite);
jitSuite(InElementSuite);
+jitSuite(InElementDocumentFragmentSuite);
jitComponentSuite(GlimmerishComponents);
jitComponentSuite(TemplateOnlyComponents);
diff --git a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts
index 47919a1e7ac..3a7dd336a26 100644
--- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts
@@ -20,7 +20,7 @@ import type {
export interface AppendingBlock extends Bounds {
debug?: { first: () => Nullable; last: () => Nullable };
- openElement(element: SimpleElement): void;
+ openElement(element: SimpleNode): void;
closeElement(): void;
didAppendNode(node: SimpleNode): void;
didAppendBounds(bounds: Bounds): void;
@@ -48,11 +48,7 @@ export interface ResettableBlock extends FixedBlock {
}
export interface DOMStack {
- pushRemoteElement(
- element: SimpleElement,
- guid: string,
- insertBefore: Maybe
- ): FixedBlock;
+ pushRemoteElement(element: SimpleNode, guid: string, insertBefore: Maybe): FixedBlock;
popRemoteElement(): FixedBlock;
popElement(): void;
openElement(tag: string, _operations?: ElementOperations): SimpleElement;
@@ -101,7 +97,7 @@ export interface TreeBuilder extends Cursor, DOMStack, TreeOperations {
dom: GlimmerTreeConstruction;
updateOperations: GlimmerTreeChanges;
constructing: Nullable;
- element: SimpleElement;
+ element: SimpleNode;
hasBlocks: boolean;
debugBlocks(): AppendingBlock[];
diff --git a/packages/@glimmer/interfaces/lib/dom/bounds.d.ts b/packages/@glimmer/interfaces/lib/dom/bounds.d.ts
index e1c97f3fe5f..aee57049201 100644
--- a/packages/@glimmer/interfaces/lib/dom/bounds.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/bounds.d.ts
@@ -1,14 +1,14 @@
import type { Nullable } from '../core.js';
-import type { SimpleElement, SimpleNode } from './simple.js';
+import type { SimpleNode } from './simple.js';
export interface Bounds {
// a method to future-proof for wormholing; may not be needed ultimately
- parentElement(): SimpleElement;
+ parentElement(): SimpleNode;
firstNode(): SimpleNode;
lastNode(): SimpleNode;
}
export interface Cursor {
- readonly element: SimpleElement;
+ readonly element: SimpleNode;
readonly nextSibling: Nullable;
}
diff --git a/packages/@glimmer/interfaces/lib/dom/changes.d.ts b/packages/@glimmer/interfaces/lib/dom/changes.d.ts
index a847cbeeb36..43c39a174d1 100644
--- a/packages/@glimmer/interfaces/lib/dom/changes.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/changes.d.ts
@@ -3,9 +3,9 @@ import type { Bounds } from './bounds.js';
import type { Namespace, SimpleComment, SimpleElement, SimpleNode, SimpleText } from './simple.js';
export interface GlimmerDOMOperations {
- createElement(tag: string, context?: SimpleElement): SimpleElement;
- insertBefore(parent: SimpleElement, node: SimpleNode, reference: Nullable): void;
- insertHTMLBefore(parent: SimpleElement, nextSibling: Nullable, html: string): Bounds;
+ createElement(tag: string, context?: SimpleNode): SimpleElement;
+ insertBefore(parent: SimpleNode, node: SimpleNode, reference: Nullable): void;
+ insertHTMLBefore(parent: SimpleNode, nextSibling: Nullable, html: string): Bounds;
createTextNode(text: string): SimpleText;
createComment(data: string): SimpleComment;
}
@@ -13,7 +13,7 @@ export interface GlimmerDOMOperations {
export interface GlimmerTreeChanges extends GlimmerDOMOperations {
setAttribute(element: SimpleElement, name: string, value: string): void;
removeAttribute(element: SimpleElement, name: string): void;
- insertAfter(element: SimpleElement, node: SimpleNode, reference: SimpleNode): void;
+ insertAfter(element: SimpleNode, node: SimpleNode, reference: SimpleNode): void;
}
export interface GlimmerTreeConstruction extends GlimmerDOMOperations {
diff --git a/packages/@glimmer/interfaces/lib/dom/tree-construction.d.ts b/packages/@glimmer/interfaces/lib/dom/tree-construction.d.ts
index 75570d944d6..96f9b63c5da 100644
--- a/packages/@glimmer/interfaces/lib/dom/tree-construction.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/tree-construction.d.ts
@@ -1,4 +1,4 @@
-import type { Namespace, SimpleDocumentFragment, SimpleElement, SimpleNode } from './simple.js';
+import type { Namespace, SimpleNode } from './simple.js';
export type NodeToken = number;
@@ -17,5 +17,5 @@ export interface SpecTreeConstruction {
appendComment(text: string): NodeToken;
setAttribute(name: string, value: string, namespace?: Namespace): void;
- appendTo(parent: SimpleElement | SimpleDocumentFragment): NodeTokens;
+ appendTo(parent: SimpleNode): NodeTokens;
}
diff --git a/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts b/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts
index e3a7aafd754..e574f28a3d9 100644
--- a/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts
+++ b/packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts
@@ -1,4 +1,4 @@
-import type { SimpleElement, SimpleNode } from '@simple-dom/interface';
+import type { SimpleNode } from '@simple-dom/interface';
import type { Bounds } from '../dom/bounds.js';
import type { Arguments, CapturedArguments } from './arguments.js';
@@ -27,7 +27,7 @@ export interface CapturedRenderNode {
instance: unknown;
template: string | null;
bounds: null | {
- parentElement: SimpleElement;
+ parentElement: SimpleNode;
firstNode: SimpleNode;
lastNode: SimpleNode;
};
diff --git a/packages/@glimmer/interfaces/lib/runtime/render.d.ts b/packages/@glimmer/interfaces/lib/runtime/render.d.ts
index bd8ad0a2023..1c9ade3c8dd 100644
--- a/packages/@glimmer/interfaces/lib/runtime/render.d.ts
+++ b/packages/@glimmer/interfaces/lib/runtime/render.d.ts
@@ -1,4 +1,4 @@
-import type { SimpleElement, SimpleNode } from '@simple-dom/interface';
+import type { SimpleNode } from '@simple-dom/interface';
import type { RichIteratorResult } from '../core.js';
import type { Bounds } from '../dom/bounds.js';
@@ -14,7 +14,7 @@ export interface RenderResult extends Bounds, ExceptionHandler {
rerender(options?: { alwaysRevalidate: false }): void;
- parentElement(): SimpleElement;
+ parentElement(): SimpleNode;
firstNode(): SimpleNode;
lastNode(): SimpleNode;
diff --git a/packages/@glimmer/node/lib/serialize-builder.ts b/packages/@glimmer/node/lib/serialize-builder.ts
index 43a68363b75..fa3a0d52d1d 100644
--- a/packages/@glimmer/node/lib/serialize-builder.ts
+++ b/packages/@glimmer/node/lib/serialize-builder.ts
@@ -33,9 +33,12 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
private serializeBlockDepth = 0;
override __openBlock(): void {
- let { tagName } = this.element;
-
- if (tagName !== 'TITLE' && tagName !== 'SCRIPT' && tagName !== 'STYLE') {
+ if (
+ 'tagName' in this.element &&
+ this.element.tagName !== 'TITLE' &&
+ this.element.tagName !== 'SCRIPT' &&
+ this.element.tagName !== 'STYLE'
+ ) {
let depth = this.serializeBlockDepth++;
this.__appendComment(`%+b:${depth}%`);
}
@@ -44,26 +47,32 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
}
override __closeBlock(): void {
- let { tagName } = this.element;
-
super.__closeBlock();
- if (tagName !== 'TITLE' && tagName !== 'SCRIPT' && tagName !== 'STYLE') {
+ if (
+ 'tagName' in this.element &&
+ this.element.tagName !== 'TITLE' &&
+ this.element.tagName !== 'SCRIPT' &&
+ this.element.tagName !== 'STYLE'
+ ) {
let depth = --this.serializeBlockDepth;
this.__appendComment(`%-b:${depth}%`);
}
}
override __appendHTML(html: string): Bounds {
- let { tagName } = this.element;
-
- if (tagName === 'TITLE' || tagName === 'SCRIPT' || tagName === 'STYLE') {
+ if (
+ 'tagName' in this.element &&
+ (this.element.tagName === 'TITLE' ||
+ this.element.tagName === 'SCRIPT' ||
+ this.element.tagName === 'STYLE')
+ ) {
return super.__appendHTML(html);
}
// Do we need to run the html tokenizer here?
let first = this.__appendComment('%glmr%');
- if (tagName === 'TABLE') {
+ if ('tagName' in this.element && this.element.tagName === 'TABLE') {
let openIndex = html.indexOf('<');
if (openIndex > -1) {
let tr = html.slice(openIndex + 1, openIndex + 3);
@@ -83,10 +92,14 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
}
override __appendText(string: string): SimpleText {
- let { tagName } = this.element;
let current = currentNode(this);
- if (tagName === 'TITLE' || tagName === 'SCRIPT' || tagName === 'STYLE') {
+ if (
+ 'tagName' in this.element &&
+ (this.element.tagName === 'TITLE' ||
+ this.element.tagName === 'SCRIPT' ||
+ this.element.tagName === 'STYLE')
+ ) {
return super.__appendText(string);
} else if (string === '') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -110,6 +123,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
override openElement(tag: string) {
if (tag === 'tr') {
if (
+ 'tagName' in this.element &&
this.element.tagName !== 'TBODY' &&
this.element.tagName !== 'THEAD' &&
this.element.tagName !== 'TFOOT'
@@ -129,7 +143,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
}
override pushRemoteElement(
- element: SimpleElement,
+ element: SimpleNode,
cursorId: string,
insertBefore: Maybe = null
): RemoteBlock {
@@ -143,7 +157,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
export function serializeBuilder(
env: Environment,
- cursor: { element: SimpleElement; nextSibling: Nullable }
+ cursor: { element: SimpleNode; nextSibling: Nullable }
): TreeBuilder {
return SerializeBuilder.forInitialRender(env, cursor);
}
diff --git a/packages/@glimmer/runtime/lib/bounds.ts b/packages/@glimmer/runtime/lib/bounds.ts
index 784ed5ca890..5e8376710fb 100644
--- a/packages/@glimmer/runtime/lib/bounds.ts
+++ b/packages/@glimmer/runtime/lib/bounds.ts
@@ -3,7 +3,7 @@ import { expect, setLocalDebugType } from '@glimmer/debug-util';
export class CursorImpl implements Cursor {
constructor(
- public element: SimpleElement,
+ public element: SimpleNode,
public nextSibling: Nullable
) {
setLocalDebugType('cursor', this);
@@ -14,12 +14,12 @@ export type DestroyableBounds = Bounds;
export class ConcreteBounds implements Bounds {
constructor(
- public parentNode: SimpleElement,
+ public parentNode: SimpleNode,
private first: SimpleNode,
private last: SimpleNode
) {}
- parentElement(): SimpleElement {
+ parentElement(): SimpleNode {
return this.parentNode;
}
@@ -53,10 +53,15 @@ export function move(bounds: Bounds, reference: Nullable): Nullable<
}
export function clear(bounds: Bounds): Nullable {
- let parent = bounds.parentElement();
let first = bounds.firstNode();
let last = bounds.lastNode();
+ // Use the node's actual current parent rather than the stored parentElement.
+ // When bounds were rendered into a DocumentFragment that was subsequently
+ // appended to a real DOM container, the nodes' parentNode is the container
+ // while parentElement() still returns the (now-empty) fragment.
+ let parent = (first.parentNode as Nullable) ?? bounds.parentElement();
+
let current: SimpleNode = first;
while (true) {
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
index 9c0d378b123..8a4870cbdd7 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
@@ -29,10 +29,12 @@ import {
} from '@glimmer/constants';
import {
check,
+ CheckDocumentFragment,
CheckElement,
CheckMaybe,
CheckNode,
CheckNullable,
+ CheckOr,
CheckString,
} from '@glimmer/debug';
import { debugToString, expect } from '@glimmer/debug-util';
@@ -74,7 +76,7 @@ APPEND_OPCODES.add(VM_PUSH_REMOTE_ELEMENT_OP, (vm) => {
let insertBeforeRef = check(vm.stack.pop(), CheckReference);
let guidRef = check(vm.stack.pop(), CheckReference);
- let element = check(valueForRef(elementRef), CheckElement);
+ let element = check(valueForRef(elementRef), CheckOr(CheckElement, CheckDocumentFragment));
let insertBefore = check(valueForRef(insertBeforeRef), CheckMaybe(CheckNullable(CheckNode)));
let guid = valueForRef(guidRef) as string;
diff --git a/packages/@glimmer/runtime/lib/vm/element-builder.ts b/packages/@glimmer/runtime/lib/vm/element-builder.ts
index 160944fefa4..2fc514af4f9 100644
--- a/packages/@glimmer/runtime/lib/vm/element-builder.ts
+++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts
@@ -61,7 +61,7 @@ export class Fragment implements Bounds {
this.bounds = bounds;
}
- parentElement(): SimpleElement {
+ parentElement(): SimpleNode {
return this.bounds.parentElement();
}
@@ -96,7 +96,12 @@ export class NewTreeBuilder implements TreeBuilder {
}
static resume(env: Environment, block: ResettableBlock): NewTreeBuilder {
- let parentNode = block.parentElement();
+ // Capture the live parent before resetting, because the bounds may have been
+ // rendered into a DocumentFragment that was subsequently appended to a real
+ // DOM container. In that case firstNode().parentNode is the container while
+ // parentElement() still returns the original (now-empty) fragment.
+ let parentNode =
+ (block.firstNode().parentNode as SimpleElement | null) ?? block.parentElement();
let nextSibling = block.reset(env);
let stack = new this(env, parentNode, nextSibling).initialize();
@@ -105,7 +110,7 @@ export class NewTreeBuilder implements TreeBuilder {
return stack;
}
- constructor(env: Environment, parentNode: SimpleElement, nextSibling: Nullable) {
+ constructor(env: Environment, parentNode: SimpleNode, nextSibling: Nullable) {
this.pushElement(parentNode, nextSibling);
this.env = env;
this.dom = env.getAppendOperations();
@@ -129,7 +134,7 @@ export class NewTreeBuilder implements TreeBuilder {
return this.blockStack.toArray();
}
- get element(): SimpleElement {
+ get element(): SimpleNode {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return this.cursors.current!.element;
}
@@ -216,7 +221,7 @@ export class NewTreeBuilder implements TreeBuilder {
this.didOpenElement(element);
}
- __flushElement(parent: SimpleElement, constructing: SimpleElement) {
+ __flushElement(parent: SimpleNode, constructing: SimpleElement) {
this.dom.insertBefore(parent, constructing, this.nextSibling);
}
@@ -227,7 +232,7 @@ export class NewTreeBuilder implements TreeBuilder {
}
pushRemoteElement(
- element: SimpleElement,
+ element: SimpleNode,
guid: string,
insertBefore: Maybe
): RemoteBlock {
@@ -235,7 +240,7 @@ export class NewTreeBuilder implements TreeBuilder {
}
__pushRemoteElement(
- element: SimpleElement,
+ element: SimpleNode,
_guid: string,
insertBefore: Maybe
): RemoteBlock {
@@ -259,7 +264,7 @@ export class NewTreeBuilder implements TreeBuilder {
return block;
}
- protected pushElement(element: SimpleElement, nextSibling: Maybe = null): void {
+ protected pushElement(element: SimpleNode, nextSibling: Maybe = null): void {
this.cursors.push(new CursorImpl(element, nextSibling));
}
@@ -400,7 +405,7 @@ export class AppendingBlockImpl implements AppendingBlock {
protected last: Nullable = null;
protected nesting = 0;
- constructor(private parent: SimpleElement) {
+ constructor(private parent: SimpleNode) {
setLocalDebugType('block:simple', this);
if (LOCAL_DEBUG) {
@@ -470,7 +475,7 @@ export class AppendingBlockImpl implements AppendingBlock {
}
export class RemoteBlock extends AppendingBlockImpl {
- constructor(parent: SimpleElement) {
+ constructor(parent: SimpleNode) {
super(parent);
setLocalDebugType('block:remote', this);
@@ -500,7 +505,12 @@ export class RemoteBlock extends AppendingBlockImpl {
// and avoid clearing the node if it was. In most cases this shouldn't happen,
// so this might hide bugs where the code clears nested nodes unnecessarily,
// so we should eventually try to do the correct fix.
- if (this.parentElement() === this.firstNode().parentNode) {
+ //
+ // Note: we check firstNode().parentNode !== null (node still has a parent)
+ // rather than === parentElement() (node is in the original parent), so that
+ // {{#in-element}} into a DocumentFragment still clears correctly after the
+ // fragment's children are moved to a real DOM container via appendChild().
+ if (this.firstNode().parentNode !== null) {
clear(this);
}
});
@@ -508,7 +518,7 @@ export class RemoteBlock extends AppendingBlockImpl {
}
export class ResettableBlockImpl extends AppendingBlockImpl implements ResettableBlock {
- constructor(parent: SimpleElement) {
+ constructor(parent: SimpleNode) {
super(parent);
setLocalDebugType('block:resettable', this);
}
@@ -528,7 +538,7 @@ export class ResettableBlockImpl extends AppendingBlockImpl implements Resettabl
// FIXME: All the noops in here indicate a modelling problem
export class AppendingBlockList implements AppendingBlock {
constructor(
- private readonly parent: SimpleElement,
+ private readonly parent: SimpleNode,
public boundList: AppendingBlock[]
) {
this.parent = parent;
diff --git a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
index 60f8b061781..53d1074f6ec 100644
--- a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
+++ b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
@@ -196,15 +196,18 @@ export class RehydrateTree extends NewTreeBuilder implements TreeBuilder {
const { candidate } = currentCursor;
if (candidate === null) return;
- const { tagName } = currentCursor.element;
-
if (
isOpenBlock(candidate) &&
getBlockDepthWithOffset(candidate, this.startingBlockOffset) === blockDepth
) {
this.candidate = this.remove(candidate);
currentCursor.openBlockDepth = blockDepth;
- } else if (tagName !== 'TITLE' && tagName !== 'SCRIPT' && tagName !== 'STYLE') {
+ } else if (
+ 'tagName' in currentCursor.element &&
+ currentCursor.element.tagName !== 'TITLE' &&
+ currentCursor.element.tagName !== 'SCRIPT' &&
+ currentCursor.element.tagName !== 'STYLE'
+ ) {
this.clearMismatch(candidate);
}
}
diff --git a/packages/@glimmer/runtime/lib/vm/render-result.ts b/packages/@glimmer/runtime/lib/vm/render-result.ts
index bb2129ddfa7..9ad1fa92613 100644
--- a/packages/@glimmer/runtime/lib/vm/render-result.ts
+++ b/packages/@glimmer/runtime/lib/vm/render-result.ts
@@ -2,7 +2,6 @@ import type {
AppendingBlock,
Environment,
RenderResult,
- SimpleElement,
SimpleNode,
UpdatingOpcode,
} from '@glimmer/interfaces';
@@ -29,7 +28,7 @@ export default class RenderResultImpl implements RenderResult {
vm.execute(updating, this);
}
- parentElement(): SimpleElement {
+ parentElement(): SimpleNode {
return this.bounds.parentElement();
}