Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@ember/-internals/glimmer/lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,7 @@ export class Renderer extends BaseRenderer {
}

getBounds(component: View): {
parentElement: SimpleElement;
parentElement: SimpleNode;
firstNode: SimpleNode;
lastNode: SimpleNode;
} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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}}<p id="frag-p">{{this.message}}</p>{{/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);
Comment thread
NullVoxPopuli marked this conversation as resolved.
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}}' +
'<p id="msg">{{this.step this.message}}</p>' +
'{{#if this.show}}' +
'<span id="extra">extra {{this.step "extra rendered"}}</span>' +
'{{/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}}<p id="a">{{this.foo}}</p>{{/in-element}}' +
'{{#in-element this.fragment insertBefore=null}}<p id="b">{{this.bar}}</p>{{/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('<!----><!---->');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GlimmerishComponents,
HasBlockParamsHelperSuite,
HasBlockSuite,
InElementDocumentFragmentSuite,
InElementSuite,
jitComponentSuite,
jitSuite,
Expand All @@ -18,6 +19,7 @@ import {
jitComponentSuite(DebuggerSuite);
jitSuite(EachSuite);
jitSuite(InElementSuite);
jitSuite(InElementDocumentFragmentSuite);

jitComponentSuite(GlimmerishComponents);
jitComponentSuite(TemplateOnlyComponents);
Expand Down
10 changes: 3 additions & 7 deletions packages/@glimmer/interfaces/lib/dom/attributes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
export interface AppendingBlock extends Bounds {
debug?: { first: () => Nullable<SimpleNode>; last: () => Nullable<SimpleNode> };

openElement(element: SimpleElement): void;
openElement(element: SimpleNode): void;
closeElement(): void;
didAppendNode(node: SimpleNode): void;
didAppendBounds(bounds: Bounds): void;
Expand Down Expand Up @@ -48,11 +48,7 @@ export interface ResettableBlock extends FixedBlock {
}

export interface DOMStack {
pushRemoteElement(
element: SimpleElement,
guid: string,
insertBefore: Maybe<SimpleNode>
): FixedBlock;
pushRemoteElement(element: SimpleNode, guid: string, insertBefore: Maybe<SimpleNode>): FixedBlock;
popRemoteElement(): FixedBlock;
popElement(): void;
openElement(tag: string, _operations?: ElementOperations): SimpleElement;
Expand Down Expand Up @@ -101,7 +97,7 @@ export interface TreeBuilder extends Cursor, DOMStack, TreeOperations {
dom: GlimmerTreeConstruction;
updateOperations: GlimmerTreeChanges;
constructing: Nullable<SimpleElement>;
element: SimpleElement;
element: SimpleNode;

hasBlocks: boolean;
debugBlocks(): AppendingBlock[];
Expand Down
6 changes: 3 additions & 3 deletions packages/@glimmer/interfaces/lib/dom/bounds.d.ts
Original file line number Diff line number Diff line change
@@ -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<SimpleNode>;
}
8 changes: 4 additions & 4 deletions packages/@glimmer/interfaces/lib/dom/changes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ 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<SimpleNode>): void;
insertHTMLBefore(parent: SimpleElement, nextSibling: Nullable<SimpleNode>, html: string): Bounds;
createElement(tag: string, context?: SimpleNode): SimpleElement;
insertBefore(parent: SimpleNode, node: SimpleNode, reference: Nullable<SimpleNode>): void;
insertHTMLBefore(parent: SimpleNode, nextSibling: Nullable<SimpleNode>, html: string): Bounds;
createTextNode(text: string): SimpleText;
createComment(data: string): SimpleComment;
}

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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/@glimmer/interfaces/lib/dom/tree-construction.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Namespace, SimpleDocumentFragment, SimpleElement, SimpleNode } from './simple.js';
import type { Namespace, SimpleNode } from './simple.js';

export type NodeToken = number;

Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -27,7 +27,7 @@ export interface CapturedRenderNode {
instance: unknown;
template: string | null;
bounds: null | {
parentElement: SimpleElement;
parentElement: SimpleNode;
firstNode: SimpleNode;
lastNode: SimpleNode;
};
Expand Down
4 changes: 2 additions & 2 deletions packages/@glimmer/interfaces/lib/runtime/render.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,7 +14,7 @@ export interface RenderResult extends Bounds, ExceptionHandler {

rerender(options?: { alwaysRevalidate: false }): void;

parentElement(): SimpleElement;
parentElement(): SimpleNode;

firstNode(): SimpleNode;
lastNode(): SimpleNode;
Expand Down
Loading
Loading