{{yield (hash
@@ -1151,7 +1151,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
`{{(has-block-params)}}` indicates if the component was invoked with block params.
This component is invoked with block params:
-
+
```handlebars
Hi Jen!
@@ -1243,33 +1243,33 @@ export interface NotHelper extends Opaque<'helper:not'> {}
additional information on how a `Component` functions.
This is similar to the concept of Partial Application.
-
+
For example, given a `FullName` component:
-
+
```gjs {data-filename="app/components/full-name.gjs"}
import MyInputComponent from './my-input-component';
-
+
{{yield (component MyInputComponent value=@model.name placeholder="Username")}}
```
-
+
The yielded component can be invoked by the calling component.
See the following snippet:
-
+
```gjs {data-filename="app/components/person-form.gjs"}
import FullName from './full-name';
-
+
```
-
+
Which will output an input whose value is already bound to `@model.name` and `placeholder`
is "Username".
-
+
Any arguments passed at the invocation site of the component will override those applied via
the `component` helper. For example, if the invocation site of the component is:
@@ -1285,9 +1285,9 @@ export interface NotHelper extends Opaque<'helper:not'> {}
The output will be an input whose value is bound to `@model.name` and `placeholder`
is "Your name".
-
- The `component` helper is built-in and does not need to be imported.
-
+
+ The `component` helper is built-in and does not need to be imported.
+
Prior to Strict Mode aka "Template Tag" or gjs, the component helper was also used to invoke
components dynamically. This is no longer necessary, and they can be directly invoked, as above.
@@ -1310,7 +1310,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
{{!-- The component can be invoked directly --}}
-
+
{{!-- The component helper here is no longer necessary --}}
{{component this.infographicComponentName}}
@@ -1382,7 +1382,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs {data-filename="app/templates/application.gjs"}
import Weather from '../components/weather';
-
+
@@ -1424,14 +1424,14 @@ export interface NotHelper extends Opaque<'helper:not'> {}
{{else}}
No, it's lovely outside!
{{/if}}
-
+
```
If you call it by saying `isCold` is true:
```gjs {data-filename="app/templates/application.gjs"}
import Weather from '../components/weather';
-
+
@@ -1450,7 +1450,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs {data-filename="app/templates/application.gjs"}
import Greeting from '../components/greeting';
-
+
@@ -1471,9 +1471,9 @@ export interface NotHelper extends Opaque<'helper:not'> {}
One detail to keep in mind is that both branches of the `if` helper will be evaluated,
so if you have `{{if condition "foo" (expensive-operation "bar")`,
`expensive-operation` will always calculate.
-
+
`if` is built-in and does not need to be imported.
-
+
@method if
@static
@for Keywords
@@ -1497,7 +1497,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs {data-filename="app/templates/application.gjs"}
import Greeting from '../components/greeting';
-
+
@@ -1523,7 +1523,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs {data-filename="app/templates/application.gjs"}
import Greeting from '../components/greeting';
-
+
@@ -1556,7 +1556,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs {data-filename="app/templates/application.gjs"}
import LoggedIn from '../components/logged-in';
-
+
@@ -1583,9 +1583,9 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```html
Please login.
```
-
+
`unless` is built-in and does not need to be imported.
-
+
@method unless
@for Keywords
@noimport
@@ -1600,7 +1600,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```handlebars
{{log "myVariable:" myVariable }}
```
-
+
`log` is built-in as a template keyword and does not need to be imported.
@method log
@@ -1643,7 +1643,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
these arguments will be passed along to the referenced modifier.
`modifier` is built-in as a template keyword and does not need to be imported.
-
+
@method modifier
@for Keywords
@static
@@ -1658,7 +1658,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```gjs
import { pageTitle } from 'ember-page-title';
-
+
{{pageTitle "My Page Title" }}
@@ -1703,7 +1703,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```
`let` is built-in and does not need to be imported.
-
+
@method let
@static
@noimport
@@ -1730,8 +1730,8 @@ export interface NotHelper extends Opaque<'helper:not'> {}
`{{in-element}}` requires a single positional argument:
- - `destinationElement` -- the DOM element to render into. It must exist at the time
- of rendering.
+ - `destinationElement` -- the DOM element, `DocumentFragment`, or `ShadowRoot` to
+ render into. It must exist at the time of rendering.
It also supports an optional named argument:
@@ -1749,7 +1749,7 @@ export interface NotHelper extends Opaque<'helper:not'> {}
```
`in-element` is built-in and does not need to be imported.
-
+
@method in-element
@for Keywords
@param {Element} destinationElement the DOM element to render into. It must exist at the time
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..ad35f4f0eb0
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts
@@ -0,0 +1,352 @@
+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
+ 'Conditional content follows the fragment after it is attached to the DOM'() {
+ const fragment = document.createDocumentFragment();
+ const container = document.createElement('div');
+
+ this.render(
+ '{{#in-element this.fragment}}' +
+ 'stable
' +
+ '{{#if this.show}}cond{{/if}}' +
+ '{{/in-element}}',
+ {
+ fragment,
+ show: false,
+ }
+ );
+
+ container.appendChild(fragment);
+
+ // The conditional block was empty at attach time; toggling it on must
+ // render the new element into the container (where the surrounding content
+ // now lives), not into the now-empty fragment.
+ this.rerender({ show: true });
+ this.assert.ok(container.querySelector('#cond'), 'conditional element rendered in container');
+ this.assert.strictEqual(fragment.childNodes.length, 0, 'nothing was rendered into fragment');
+ this.assert.strictEqual(
+ container.querySelector('#stable')?.nextSibling?.nodeName,
+ 'SPAN',
+ 'conditional element is in position, next to the stable element'
+ );
+
+ this.rerender({ show: false });
+ this.assert.notOk(container.querySelector('#cond'), 'conditional element removed');
+ this.assert.ok(container.querySelector('#stable'), 'stable element remains');
+ this.assert.strictEqual(fragment.childNodes.length, 0, 'fragment is still empty');
+ }
+
+ @test
+ 'Destroying {{#in-element}} clears the container after the fragment is attached'() {
+ const fragment = document.createDocumentFragment();
+ const container = document.createElement('div');
+
+ this.render(
+ '{{#if this.showing}}' +
+ '{{#in-element this.fragment}}hello
{{/in-element}}' +
+ '{{/if}}',
+ {
+ fragment,
+ showing: true,
+ }
+ );
+
+ container.appendChild(fragment);
+ this.assert.ok(container.querySelector('#content'), 'content is in container after append');
+
+ // Tearing down the {{#in-element}} must remove the content from wherever it
+ // currently lives (the container), not from the stale fragment.
+ this.rerender({ showing: false });
+ this.assert.notOk(container.querySelector('#content'), 'content removed from container');
+ this.assert.strictEqual(container.childNodes.length, 0, 'container is empty after destroy');
+
+ this.rerender({ showing: true });
+ this.assert.strictEqual(
+ fragment.querySelector('#content')?.textContent,
+ 'hello',
+ 'content renders into the fragment again when in-element comes back'
+ );
+ }
+
+ @test
+ '{{#each}} updates follow the content after the fragment is attached'() {
+ const fragment = document.createDocumentFragment();
+ const container = document.createElement('div');
+ const text = (parent: ParentNode) =>
+ Array.from(parent.querySelectorAll('span.item'))
+ .map((node) => node.textContent)
+ .join('');
+
+ this.render(
+ '{{#in-element this.fragment}}' +
+ '{{#each this.items as |item|}}{{item}}{{/each}}' +
+ '{{/in-element}}',
+ {
+ fragment,
+ items: ['a', 'b'],
+ }
+ );
+
+ this.assert.strictEqual(text(fragment), 'ab', 'initial items rendered into fragment');
+
+ container.appendChild(fragment);
+
+ this.rerender({ items: ['a', 'b', 'c'] });
+ this.assert.strictEqual(text(container), 'abc', 'new item appended in container');
+ this.assert.strictEqual(fragment.childNodes.length, 0, 'nothing was rendered into fragment');
+
+ this.rerender({ items: ['c', 'b', 'a'] });
+ this.assert.strictEqual(text(container), 'cba', 'items reordered in container');
+ this.assert.strictEqual(fragment.childNodes.length, 0, 'fragment is still empty');
+
+ this.rerender({ items: ['b'] });
+ this.assert.strictEqual(text(container), 'b', 'items removed in container');
+ }
+
+ @test
+ 'Renders into a ShadowRoot (a DocumentFragment subtype)'() {
+ const host = document.createElement('div');
+ const shadowRoot = host.attachShadow({ mode: 'open' });
+
+ this.render(
+ '{{#in-element this.shadowRoot}}{{this.message}}
{{/in-element}}',
+ {
+ shadowRoot,
+ message: 'hello',
+ }
+ );
+
+ this.assert.strictEqual(
+ shadowRoot.querySelector('#in-shadow')?.textContent,
+ 'hello',
+ 'content rendered into shadow root'
+ );
+ this.assertHTML('');
+ this.assertStableRerender();
+
+ this.rerender({ message: 'updated' });
+ this.assert.strictEqual(
+ shadowRoot.querySelector('#in-shadow')?.textContent,
+ 'updated',
+ 'content updated in shadow root'
+ );
+ }
+
+ @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/ember-component-test.ts b/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts
index 5f1c5f4c509..56728ebf5de 100644
--- a/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts
+++ b/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts
@@ -2165,7 +2165,7 @@ class CurlyBoundsTrackingTest extends CurlyTest {
const { bounds, element } = instance.captured;
assert.strictEqual(
- bounds.parentElement(),
+ bounds.parentNode(),
document.querySelector('#qunit-fixture') as unknown as SimpleElement
);
assert.strictEqual(bounds.firstNode(), castToSimple(element));
@@ -2201,7 +2201,7 @@ class CurlyBoundsTrackingTest extends CurlyTest {
const { bounds } = instance.captured;
assert.strictEqual(
- check(bounds.parentElement(), HTMLElement),
+ check(bounds.parentNode(), HTMLElement),
check(document.querySelector('#qunit-fixture'), HTMLElement)
);
assert.strictEqual(
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..74d7ff2b5c5 100644
--- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts
@@ -49,7 +49,7 @@ export interface ResettableBlock extends FixedBlock {
export interface DOMStack {
pushRemoteElement(
- element: SimpleElement,
+ element: SimpleElement | SimpleDocumentFragment,
guid: string,
insertBefore: Maybe
): FixedBlock;
@@ -78,7 +78,7 @@ export interface DOMStack {
export interface TreeOperations {
__openElement(tag: string): SimpleElement;
- __flushElement(parent: SimpleElement, constructing: SimpleElement): void;
+ __flushElement(parent: SimpleElement | SimpleDocumentFragment, constructing: SimpleElement): void;
__openBlock(): void;
__closeBlock(): void;
__appendText(text: string): SimpleText;
@@ -101,7 +101,7 @@ export interface TreeBuilder extends Cursor, DOMStack, TreeOperations {
dom: GlimmerTreeConstruction;
updateOperations: GlimmerTreeChanges;
constructing: Nullable;
- element: SimpleElement;
+ element: SimpleElement | SimpleDocumentFragment;
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..d050de60d02 100644
--- a/packages/@glimmer/interfaces/lib/dom/bounds.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/bounds.d.ts
@@ -1,14 +1,16 @@
import type { Nullable } from '../core.js';
-import type { SimpleElement, SimpleNode } from './simple.js';
+import type { SimpleDocumentFragment, SimpleElement, SimpleNode } from './simple.js';
export interface Bounds {
- // a method to future-proof for wormholing; may not be needed ultimately
- parentElement(): SimpleElement;
+ // Like DOM's parentNode, this may be an Element, a DocumentFragment, or a
+ // Document (DOM's parentElement is always an Element or null, which is why
+ // this is not named parentElement).
+ parentNode(): SimpleElement | SimpleDocumentFragment;
firstNode(): SimpleNode;
lastNode(): SimpleNode;
}
export interface Cursor {
- readonly element: SimpleElement;
+ readonly element: SimpleElement | SimpleDocumentFragment;
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..34314b1dbd3 100644
--- a/packages/@glimmer/interfaces/lib/dom/changes.d.ts
+++ b/packages/@glimmer/interfaces/lib/dom/changes.d.ts
@@ -1,11 +1,26 @@
import type { Nullable } from '../core.js';
import type { Bounds } from './bounds.js';
-import type { Namespace, SimpleComment, SimpleElement, SimpleNode, SimpleText } from './simple.js';
+import type {
+ Namespace,
+ SimpleComment,
+ SimpleDocumentFragment,
+ 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?: SimpleElement | SimpleDocumentFragment): SimpleElement;
+ insertBefore(
+ parent: SimpleElement | SimpleDocumentFragment,
+ node: SimpleNode,
+ reference: Nullable
+ ): void;
+ insertHTMLBefore(
+ parent: SimpleElement | SimpleDocumentFragment,
+ nextSibling: Nullable,
+ html: string
+ ): Bounds;
createTextNode(text: string): SimpleText;
createComment(data: string): SimpleComment;
}
@@ -13,7 +28,11 @@ 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: SimpleElement | SimpleDocumentFragment,
+ node: SimpleNode,
+ reference: SimpleNode
+ ): void;
}
export interface GlimmerTreeConstruction extends GlimmerDOMOperations {
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 7f5bcc2c9ec..fdc72df83ac 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 { SimpleDocumentFragment, SimpleElement, SimpleNode } from '@simple-dom/interface';
import type { Bounds } from '../dom/bounds.js';
import type { Arguments, CapturedArguments } from './arguments.js';
@@ -25,7 +25,7 @@ export interface CapturedRenderNode {
args: Arguments;
instance: unknown;
bounds: null | {
- parentElement: SimpleElement;
+ parentElement: SimpleElement | SimpleDocumentFragment;
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..471c9cc811f 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 { SimpleDocumentFragment, SimpleElement, 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;
+ parentNode(): SimpleElement | SimpleDocumentFragment;
firstNode(): SimpleNode;
lastNode(): SimpleNode;
diff --git a/packages/@glimmer/node/lib/serialize-builder.ts b/packages/@glimmer/node/lib/serialize-builder.ts
index b75499cb299..fd99fe4e7ad 100644
--- a/packages/@glimmer/node/lib/serialize-builder.ts
+++ b/packages/@glimmer/node/lib/serialize-builder.ts
@@ -5,6 +5,7 @@ import type {
Maybe,
ModifierInstance,
Nullable,
+ SimpleDocumentFragment,
SimpleElement,
SimpleNode,
SimpleText,
@@ -19,7 +20,7 @@ const TEXT_NODE = 3;
const NEEDS_EXTRA_CLOSE = new WeakMap();
function currentNode(
- cursor: TreeBuilder | { element: SimpleElement; nextSibling: SimpleNode }
+ cursor: TreeBuilder | { element: SimpleElement | SimpleDocumentFragment; nextSibling: SimpleNode }
): Nullable {
let { element, nextSibling } = cursor;
@@ -34,9 +35,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}%`);
}
@@ -45,26 +49,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);
@@ -84,10 +94,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
@@ -111,6 +125,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'
@@ -130,7 +145,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
}
override pushRemoteElement(
- element: SimpleElement,
+ element: SimpleElement | SimpleDocumentFragment,
cursorId: string,
insertBefore: Maybe = null
): RemoteBlock {
@@ -144,7 +159,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder {
export function serializeBuilder(
env: Environment,
- cursor: { element: SimpleElement; nextSibling: Nullable }
+ cursor: { element: SimpleElement | SimpleDocumentFragment; 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 224a45cb067..5f17efe9073 100644
--- a/packages/@glimmer/runtime/lib/bounds.ts
+++ b/packages/@glimmer/runtime/lib/bounds.ts
@@ -1,10 +1,18 @@
-import type { Bounds, Cursor, Nullable, SimpleElement, SimpleNode } from '@glimmer/interfaces';
+import type {
+ Bounds,
+ Cursor,
+ Maybe,
+ Nullable,
+ SimpleDocumentFragment,
+ SimpleElement,
+ SimpleNode,
+} from '@glimmer/interfaces';
import { expect } from '@glimmer/debug-util/lib/platform-utils';
import { setLocalDebugType } from '@glimmer/debug-util/lib/debug-brand';
export class CursorImpl implements Cursor {
constructor(
- public element: SimpleElement,
+ public element: SimpleElement | SimpleDocumentFragment,
public nextSibling: Nullable
) {
setLocalDebugType('cursor', this);
@@ -15,13 +23,13 @@ export type DestroyableBounds = Bounds;
export class ConcreteBounds implements Bounds {
constructor(
- public parentNode: SimpleElement,
+ private parent: SimpleElement | SimpleDocumentFragment,
private first: SimpleNode,
private last: SimpleNode
) {}
- parentElement(): SimpleElement {
- return this.parentNode;
+ parentNode(): SimpleElement | SimpleDocumentFragment {
+ return this.parent;
}
firstNode(): SimpleNode {
@@ -33,8 +41,29 @@ export class ConcreteBounds implements Bounds {
}
}
+/**
+ * `SimpleNode#parentNode` is typed as any node, but the DOM only ever reports an
+ * element, a fragment, or a document as a parent, and Glimmer never appends
+ * directly to a document. The parent is therefore always an insertion point.
+ */
+export function parentOf(
+ node: Maybe
+): Nullable {
+ return (node?.parentNode ?? null) as Nullable;
+}
+
+/**
+ * The parent to mutate through: normally the stored parentNode(), but when the
+ * bounds were rendered into a DocumentFragment that was later appended to the
+ * DOM, the nodes' live parentNode is the container while the stored parent is
+ * the (now-empty) fragment.
+ */
+export function liveParent(bounds: Bounds): SimpleElement | SimpleDocumentFragment {
+ return parentOf(bounds.firstNode()) ?? bounds.parentNode();
+}
+
export function move(bounds: Bounds, reference: Nullable): Nullable {
- let parent = bounds.parentElement();
+ let parent = liveParent(bounds);
let first = bounds.firstNode();
let last = bounds.lastNode();
@@ -54,7 +83,7 @@ export function move(bounds: Bounds, reference: Nullable): Nullable<
}
export function clear(bounds: Bounds): Nullable {
- let parent = bounds.parentElement();
+ let parent = liveParent(bounds);
let first = bounds.firstNode();
let last = bounds.lastNode();
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
index e12d7f8fbfb..01e91bbda1b 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts
@@ -30,10 +30,12 @@ import {
} from '@glimmer/constants/lib/syscall-ops';
import {
check,
+ CheckDocumentFragment,
CheckElement,
CheckMaybe,
CheckNode,
CheckNullable,
+ CheckOr,
CheckString,
} from '@glimmer/debug/lib/stack-check';
import debugToString from '@glimmer/debug-util/lib/debug-to-string';
@@ -77,7 +79,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/debug-render-tree.ts b/packages/@glimmer/runtime/lib/debug-render-tree.ts
index a1994ffbe27..449e2276ba1 100644
--- a/packages/@glimmer/runtime/lib/debug-render-tree.ts
+++ b/packages/@glimmer/runtime/lib/debug-render-tree.ts
@@ -191,7 +191,7 @@ export default class DebugRenderTreeImpl<
private captureBounds(node: InternalRenderNode): CapturedRenderNode['bounds'] {
let bounds = expect(node.bounds, 'BUG: missing bounds');
- let parentElement = bounds.parentElement();
+ let parentElement = bounds.parentNode();
let firstNode = bounds.firstNode();
let lastNode = bounds.lastNode();
return { parentElement, firstNode, lastNode };
diff --git a/packages/@glimmer/runtime/lib/vm/element-builder.ts b/packages/@glimmer/runtime/lib/vm/element-builder.ts
index 1e3a21359bc..2a77fa925b4 100644
--- a/packages/@glimmer/runtime/lib/vm/element-builder.ts
+++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts
@@ -27,7 +27,7 @@ import { StackImpl as Stack } from '@glimmer/util/lib/collections';
import type { DynamicAttribute } from './attributes/dynamic';
-import { clear, ConcreteBounds, CursorImpl } from '../bounds';
+import { clear, ConcreteBounds, CursorImpl, liveParent } from '../bounds';
import { dynamicAttribute } from './attributes/dynamic';
export interface FirstNode {
@@ -63,8 +63,8 @@ export class Fragment implements Bounds {
this.bounds = bounds;
}
- parentElement(): SimpleElement {
- return this.bounds.parentElement();
+ parentNode(): SimpleElement | SimpleDocumentFragment {
+ return this.bounds.parentNode();
}
firstNode(): SimpleNode {
@@ -98,7 +98,11 @@ 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
+ // parentNode() still returns the original (now-empty) fragment.
+ let parentNode = liveParent(block);
let nextSibling = block.reset(env);
let stack = new this(env, parentNode, nextSibling).initialize();
@@ -107,7 +111,11 @@ export class NewTreeBuilder implements TreeBuilder {
return stack;
}
- constructor(env: Environment, parentNode: SimpleElement, nextSibling: Nullable) {
+ constructor(
+ env: Environment,
+ parentNode: SimpleElement | SimpleDocumentFragment,
+ nextSibling: Nullable
+ ) {
this.pushElement(parentNode, nextSibling);
this.env = env;
this.dom = env.getAppendOperations();
@@ -131,7 +139,7 @@ export class NewTreeBuilder implements TreeBuilder {
return this.blockStack.toArray();
}
- get element(): SimpleElement {
+ get element(): SimpleElement | SimpleDocumentFragment {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return this.cursors.current!.element;
}
@@ -218,7 +226,7 @@ export class NewTreeBuilder implements TreeBuilder {
this.didOpenElement(element);
}
- __flushElement(parent: SimpleElement, constructing: SimpleElement) {
+ __flushElement(parent: SimpleElement | SimpleDocumentFragment, constructing: SimpleElement) {
this.dom.insertBefore(parent, constructing, this.nextSibling);
}
@@ -229,7 +237,7 @@ export class NewTreeBuilder implements TreeBuilder {
}
pushRemoteElement(
- element: SimpleElement,
+ element: SimpleElement | SimpleDocumentFragment,
guid: string,
insertBefore: Maybe
): RemoteBlock {
@@ -237,7 +245,7 @@ export class NewTreeBuilder implements TreeBuilder {
}
__pushRemoteElement(
- element: SimpleElement,
+ element: SimpleElement | SimpleDocumentFragment,
_guid: string,
insertBefore: Maybe
): RemoteBlock {
@@ -261,7 +269,10 @@ export class NewTreeBuilder implements TreeBuilder {
return block;
}
- protected pushElement(element: SimpleElement, nextSibling: Maybe = null): void {
+ protected pushElement(
+ element: SimpleElement | SimpleDocumentFragment,
+ nextSibling: Maybe = null
+ ): void {
this.cursors.push(new CursorImpl(element, nextSibling));
}
@@ -402,7 +413,7 @@ export class AppendingBlockImpl implements AppendingBlock {
protected last: Nullable = null;
protected nesting = 0;
- constructor(private parent: SimpleElement) {
+ constructor(private parent: SimpleElement | SimpleDocumentFragment) {
setLocalDebugType('block:simple', this);
if (LOCAL_DEBUG) {
@@ -413,7 +424,7 @@ export class AppendingBlockImpl implements AppendingBlock {
}
}
- parentElement() {
+ parentNode() {
return this.parent;
}
@@ -472,7 +483,7 @@ export class AppendingBlockImpl implements AppendingBlock {
}
export class RemoteBlock extends AppendingBlockImpl {
- constructor(parent: SimpleElement) {
+ constructor(parent: SimpleElement | SimpleDocumentFragment) {
super(parent);
setLocalDebugType('block:remote', this);
@@ -502,7 +513,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 === parentNode() (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);
}
});
@@ -510,7 +526,7 @@ export class RemoteBlock extends AppendingBlockImpl {
}
export class ResettableBlockImpl extends AppendingBlockImpl implements ResettableBlock {
- constructor(parent: SimpleElement) {
+ constructor(parent: SimpleElement | SimpleDocumentFragment) {
super(parent);
setLocalDebugType('block:resettable', this);
}
@@ -530,14 +546,14 @@ 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: SimpleElement | SimpleDocumentFragment,
public boundList: AppendingBlock[]
) {
this.parent = parent;
this.boundList = boundList;
}
- parentElement() {
+ parentNode() {
return this.parent;
}
diff --git a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
index ac6ab04ae8d..beb17df8f1d 100644
--- a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
+++ b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts
@@ -198,15 +198,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 32dbca888d0..41382e26c77 100644
--- a/packages/@glimmer/runtime/lib/vm/render-result.ts
+++ b/packages/@glimmer/runtime/lib/vm/render-result.ts
@@ -2,6 +2,7 @@ import type {
AppendingBlock,
Environment,
RenderResult,
+ SimpleDocumentFragment,
SimpleElement,
SimpleNode,
UpdatingOpcode,
@@ -29,8 +30,8 @@ export default class RenderResultImpl implements RenderResult {
vm.execute(updating, this);
}
- parentElement(): SimpleElement {
- return this.bounds.parentElement();
+ parentNode(): SimpleElement | SimpleDocumentFragment {
+ return this.bounds.parentNode();
}
firstNode(): SimpleNode {
diff --git a/packages/@glimmer/runtime/lib/vm/update.ts b/packages/@glimmer/runtime/lib/vm/update.ts
index 92981cc0531..bd71f2cba4b 100644
--- a/packages/@glimmer/runtime/lib/vm/update.ts
+++ b/packages/@glimmer/runtime/lib/vm/update.ts
@@ -28,7 +28,7 @@ import { resetTracking } from '@glimmer/validator/lib/tracking';
import type { Closure } from './append';
import type { AppendingBlockList } from './element-builder';
-import { clear, move as moveBounds } from '../bounds';
+import { clear, move as moveBounds, parentOf } from '../bounds';
import { NewTreeBuilder } from './element-builder';
export class UpdatingVM implements IUpdatingVM {
@@ -125,8 +125,8 @@ export abstract class BlockOpcode implements UpdatingOpcode, Bounds {
this.bounds = bounds;
}
- parentElement() {
- return this.bounds.parentElement();
+ parentNode() {
+ return this.bounds.parentNode();
}
firstNode() {
@@ -232,15 +232,12 @@ export class ListBlockOpcode extends BlockOpcode {
let { dom } = vm;
let marker = (this.marker = dom.createComment(''));
- dom.insertAfter(
- bounds.parentElement(),
- marker,
- expect(bounds.lastNode(), "can't insert after an empty bounds")
- );
+ let lastNode = expect(bounds.lastNode(), "can't insert after an empty bounds");
+ dom.insertAfter(parentOf(lastNode) ?? bounds.parentNode(), marker, lastNode);
this.sync(iterator);
- this.parentElement().removeChild(marker);
+ marker.parentNode?.removeChild(marker);
this.marker = null;
this.lastIterator = iterator;
}
@@ -357,7 +354,7 @@ export class ListBlockOpcode extends BlockOpcode {
let nextSibling = before === undefined ? this.marker : before.firstNode();
let elementStack = NewTreeBuilder.forInitialRender(env, {
- element: bounds.parentElement(),
+ element: parentOf(nextSibling) ?? bounds.parentNode(),
nextSibling,
});
diff --git a/packages/ember/tests/routing/route_manager_test.js b/packages/ember/tests/routing/route_manager_test.js
index b47883fddbd..cff978318ed 100644
--- a/packages/ember/tests/routing/route_manager_test.js
+++ b/packages/ember/tests/routing/route_manager_test.js
@@ -38,7 +38,7 @@ class RecordingRouteManager extends ClassicRouteManager {
return bucket;
}
- getInvokable(bucket,) {
+ getInvokable(bucket) {
this.log.push(['getInvokable', bucket.route.routeName]);
return super.getInvokable(bucket);
}
diff --git a/packages/internal-test-helpers/lib/test-cases/router-non-application.ts b/packages/internal-test-helpers/lib/test-cases/router-non-application.ts
index 0a96b41b6fd..ab1dc1c9487 100644
--- a/packages/internal-test-helpers/lib/test-cases/router-non-application.ts
+++ b/packages/internal-test-helpers/lib/test-cases/router-non-application.ts
@@ -13,6 +13,7 @@ import { runAppend, runDestroy } from '../run';
import type { BootOptions, EngineInstanceOptions } from '@ember/engine/instance';
import type EngineInstance from '@ember/engine/instance';
import type { InternalFactory } from '@ember/-internals/owner';
+import type { SimpleElement } from '@simple-dom/interface';
export default class RouterNonApplicationTestCase extends AbstractTestCase {
owner: EngineInstance;
@@ -43,7 +44,10 @@ export default class RouterNonApplicationTestCase extends AbstractTestCase {
let appInstance = {
renderRootComponent: (component: object) => {
setRenderer(owner, this.renderer);
- renderComponent(component, { into: this.element, owner, appendIntoTarget: true });
+ renderComponent(component, {
+ into: { element: this.element as unknown as SimpleElement, nextSibling: null },
+ owner,
+ });
},
};
owner.register('-application-instance:main', appInstance, { instantiate: false });
diff --git a/patches/@ember__test-helpers@5.4.3.patch b/patches/@ember__test-helpers@5.4.3.patch
index 1bdebd179dd..a61369d6b68 100644
--- a/patches/@ember__test-helpers@5.4.3.patch
+++ b/patches/@ember__test-helpers@5.4.3.patch
@@ -25,7 +25,7 @@ index 28a899ef8a6754f6b18aad4c557ecd048b817451..f05c2eb0c3dc378df493f4a05d7e249c
/**
@private
-@@ -55,6 +64,108 @@ function lookupOutletTemplate(owner) {
+@@ -55,6 +64,107 @@ function lookupOutletTemplate(owner) {
return OutletTemplate;
}
let templateId = 0;
@@ -71,9 +71,8 @@ index 28a899ef8a6754f6b18aad4c557ecd048b817451..f05c2eb0c3dc378df493f4a05d7e249c
+ // @TODO: Evaluate if `renderRootComponent` should allow an alternative owner
+ // as per `RenderOptions.owner` comment
+ run(() => renderComponent(component, {
-+ into: getRootElement(),
-+ owner: ownerToRenderFrom,
-+ appendIntoTarget: true
++ into: { element: getRootElement(), nextSibling: null },
++ owner: ownerToRenderFrom
+ }));
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3fb3d281121..8f5fafc7d6a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,16 +6,16 @@ settings:
overrides:
socket.io: ^4.7.0
- rollup: ^4.2.0
- ember-cli-htmlbars: ^7.0.0
+ '@ember/test-helpers': 5.4.3
babel-plugin-ember-template-compilation: ^4.0.0
ember-cli-babel: ^8.3.1
+ ember-cli-htmlbars: ^7.0.0
+ rollup: ^4.2.0
testem: ^3.20.1
- '@ember/test-helpers': 5.4.3
patchedDependencies:
'@ember/test-helpers@5.4.3':
- hash: 2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf
+ hash: 20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba
path: patches/@ember__test-helpers@5.4.3.patch
'@tracerbench/core@8.0.1':
hash: 5e48bdb11a088927d3415cc5430bb6c37d5ce66ed2dab1327914b55e4fd5cd13
@@ -2799,7 +2799,7 @@ importers:
version: 3.0.0(@types/node@22.19.19)
'@ember/test-helpers':
specifier: 5.4.3
- version: 5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7)
+ version: 5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7)
'@embroider/macros':
specifier: ^1.20.4
version: 1.20.5(@babel/core@7.29.7)
@@ -2862,7 +2862,7 @@ importers:
version: 9.0.3
ember-qunit:
specifier: ^9.1.0
- version: 9.1.0(@ember/test-helpers@5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7))(qunit@2.26.0)
+ version: 9.1.0(@ember/test-helpers@5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7))(qunit@2.26.0)
ember-resolver:
specifier: ^13.2.0
version: 13.2.0
@@ -3087,7 +3087,7 @@ importers:
version: 4.0.1
'@ember/test-helpers':
specifier: 5.4.3
- version: 5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7)
+ version: 5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7)
'@ember/test-waiters':
specifier: ^4.1.2
version: 4.1.2
@@ -3150,7 +3150,7 @@ importers:
version: 9.0.3
ember-qunit:
specifier: ^9.1.0
- version: 9.1.0(@ember/test-helpers@5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7))(qunit@2.26.0)
+ version: 9.1.0(@ember/test-helpers@5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7))(qunit@2.26.0)
ember-resolver:
specifier: ^13.2.0
version: 13.2.0
@@ -14003,7 +14003,7 @@ snapshots:
'@ember/string@4.0.1': {}
- '@ember/test-helpers@5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7)':
+ '@ember/test-helpers@5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7)':
dependencies:
'@ember/test-waiters': 4.1.2
'@embroider/addon-shim': 1.10.2
@@ -18873,9 +18873,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ember-qunit@9.1.0(@ember/test-helpers@5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7))(qunit@2.26.0):
+ ember-qunit@9.1.0(@ember/test-helpers@5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7))(qunit@2.26.0):
dependencies:
- '@ember/test-helpers': 5.4.3(patch_hash=2b4c92927238a1f8f549a6db9c31330ef24f5bd049559a1495bb07f4b57b9dbf)(@babel/core@7.29.7)
+ '@ember/test-helpers': 5.4.3(patch_hash=20764190512ce8c0fe3d816c7c17b169910a1319f845d3e28581c92a234b00ba)(@babel/core@7.29.7)
'@embroider/addon-shim': 1.10.2
qunit: 2.26.0
qunit-theme-ember: 1.0.0
diff --git a/smoke-tests/node-template/tests/node/helpers/setup-component.js b/smoke-tests/node-template/tests/node/helpers/setup-component.js
index ad994086caf..9c41ad85a34 100644
--- a/smoke-tests/node-template/tests/node/helpers/setup-component.js
+++ b/smoke-tests/node-template/tests/node/helpers/setup-component.js
@@ -95,10 +95,9 @@ function render(_template) {
if (!this._hasRendered) {
this.run(function () {
module.component = renderComponent(definition, {
- into: module.element,
+ into: { element: module.element, nextSibling: null },
owner: module.owner,
env: { document: module.element, isInteractive: false },
- appendIntoTarget: true,
});
});
this._hasRendered = true;