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/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export {
type TemplatesRegistry,
} from './lib/template_registry';
export { setupEngineRegistry, setupApplicationRegistry } from './lib/setup-registry';
export { DOMChanges, NodeDOMTreeConstruction, DOMTreeConstruction } from './lib/dom';
export { DOMRenderAdapter, NodeDOMRenderAdapter } from './lib/dom';

// needed for test
// TODO just test these through public API
Expand Down
44 changes: 26 additions & 18 deletions packages/@ember/-internals/glimmer/lib/base-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DEBUG } from '@glimmer/env';
import type {
Cursor,
DebugRenderTree,
RenderAdapter,
Environment,
RenderResult as GlimmerRenderResult,
EvaluationContext,
Expand All @@ -30,6 +31,7 @@ import type { SimpleDocument, SimpleElement } from '@simple-dom/interface';
import { hasDOM } from '../../browser-environment';
import { EmberEnvironmentDelegate } from './environment';
import ResolverImpl from './resolver';
import { adapterForDocument, getRenderAdapter } from './render-adapter';
import { renderers } from './renderers';
import { EvaluationContextImpl } from '@glimmer/opcode-compiler/lib/program-context';

Expand Down Expand Up @@ -429,13 +431,13 @@ export class BaseRenderer {
*/
static strict(
owner: object,
document: SimpleDocument | Document,
adapter: RenderAdapter,
options: { isInteractive: boolean; hasDOM?: boolean }
) {
return new BaseRenderer(
owner,
{ hasDOM: hasDOM, ...options },
document as SimpleDocument,
adapter,
new ResolverImpl(),
clientBuilder
);
Expand All @@ -446,7 +448,7 @@ export class BaseRenderer {
constructor(
owner: object,
envOptions: { isInteractive: boolean; hasDOM: boolean },
document: SimpleDocument,
adapter: RenderAdapter,
resolver: ClassicResolver,
builder: IBuilder
) {
Expand All @@ -460,7 +462,7 @@ export class BaseRenderer {
* an app needs (which will actually change and become less over time)
*/
let env = new EmberEnvironmentDelegate(owner as InternalOwner, envOptions.isInteractive);
let options = runtimeOptions({ document }, env, sharedArtifacts, resolver);
let options = runtimeOptions(adapter, env, sharedArtifacts, resolver);
let context = new EvaluationContextImpl(
sharedArtifacts,
(heap) => new RuntimeOpImpl(heap),
Expand Down Expand Up @@ -687,15 +689,16 @@ function rendererFor(owner: object, env: RenderComponentEnv | undefined): BaseRe

if (!renderer) {
/**
* SAFETY: we should figure out what we need out of a `document` and narrow the API.
* this exercise should also end up beginning to define what we need for CLI rendering (or to other outputs)
* `adapter` renders into an output that is not the DOM, for example a terminal.
*/
let document =
env && 'document' in env
? (env['document'] as SimpleDocument | Document)
: globalThis.document;

renderer = BaseRenderer.strict(owner, document, {
let adapter =
env && 'adapter' in env
? (env['adapter'] as RenderAdapter)
: env && 'document' in env
? adapterForDocument(env['document'] as SimpleDocument)
: getRenderAdapter(owner);

renderer = BaseRenderer.strict(owner, adapter, {
...env,
isInteractive: env?.isInteractive ?? true,
hasDOM: env && 'hasDOM' in env ? Boolean(env['hasDOM']) : true,
Expand All @@ -708,8 +711,6 @@ function rendererFor(owner: object, env: RenderComponentEnv | undefined): BaseRe

const isCursor = (into: IntoTarget): into is Cursor => 'element' in into;

const isDOMElement = (into: IntoTarget): into is Element => 'innerHTML' in into;

function toCursor(into: IntoTarget): Cursor {
return isCursor(into) ? into : { element: into as SimpleElement, nextSibling: null };
}
Expand All @@ -734,7 +735,11 @@ function toCursor(into: IntoTarget): Cursor {
* That render clears the element.
* A `Cursor` target is never cleared.
*/
function replaceLastRender(into: IntoTarget, element: Element | SimpleElement): IntoTarget {
function replaceLastRender(
adapter: RenderAdapter,
into: IntoTarget,
element: Element | SimpleElement
): IntoTarget {
let last = LAST_RENDER_INTO.get(element);

if (last?.glimmerResult) {
Expand All @@ -743,8 +748,8 @@ function replaceLastRender(into: IntoTarget, element: Element | SimpleElement):
return { element: element as SimpleElement, nextSibling: last.glimmerResult.firstNode() };
}

if (!last && isDOMElement(into)) {
into.innerHTML = '';
if (!last && !isCursor(into) && adapter.hasProperty(into as SimpleElement, 'innerHTML')) {
adapter.clearChildren(into as SimpleElement);
}

return into;
Expand Down Expand Up @@ -805,7 +810,10 @@ export function renderComponent(
let renderer = rendererFor(owner, env);
let element = isCursor(into) ? into.element : into;

let root = renderer.render(component, { into: replaceLastRender(into, element), args });
let root = renderer.render(component, {
into: replaceLastRender(renderer.state.env.adapter, into, element),
args,
});
let glimmerResult = root.result;

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/@ember/-internals/glimmer/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,7 +1098,11 @@ class Component<S = unknown>

let element = _element;
let isSVG = element.namespaceURI === 'http://www.w3.org/2000/svg';
let { type, normalized } = normalizeProperty(element as unknown as SimpleElement, name);
let { type, normalized } = normalizeProperty(
this.renderer._context.env.adapter,
element as unknown as SimpleElement,
name
);

if (isSVG || type === 'attr') {
return element.getAttribute(normalized);
Expand Down
5 changes: 2 additions & 3 deletions packages/@ember/-internals/glimmer/lib/dom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export { DOMChanges } from '@glimmer/runtime/lib/dom/helper';
export { DOMTreeConstruction } from '@glimmer/runtime/lib/dom/api';
export { DOMRenderAdapter } from '@glimmer/runtime/lib/dom/render-adapter';
export { clientBuilder } from '@glimmer/runtime/lib/vm/element-builder';
export { rehydrationBuilder } from '@glimmer/runtime/lib/vm/rehydrate-builder';
export { default as NodeDOMTreeConstruction } from '@glimmer/node/lib/node-dom-helper';
export { default as NodeDOMRenderAdapter } from '@glimmer/node/lib/node-dom-helper';
export { serializeBuilder } from '@glimmer/node/lib/serialize-builder';
40 changes: 40 additions & 0 deletions packages/@ember/-internals/glimmer/lib/render-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { assert } from '@ember/debug';
import type { RenderAdapter } from '@glimmer/interfaces';
import { DOMRenderAdapter } from '@glimmer/runtime/lib/dom/render-adapter';
import type { SimpleDocument } from '@simple-dom/interface';

/**
* The render adapter of each owner.
*
* This is the only way for an owner to give the renderer its adapter.
* The renderer does not read a document from the container.
*/
const ADAPTER_FOR_OWNER = new WeakMap<object, RenderAdapter>();

export function setRenderAdapter(owner: object, adapter: RenderAdapter): void {
ADAPTER_FOR_OWNER.set(owner, adapter);
}

export function adapterForDocument(document: SimpleDocument): RenderAdapter {
return new DOMRenderAdapter(document);
}

/**
* Returns the adapter of `owner`.
* An owner with no adapter gets one for the global `document`.
*/
export function getRenderAdapter(owner: object): RenderAdapter {
let adapter = ADAPTER_FOR_OWNER.get(owner);

if (adapter === undefined) {
assert(
'There is no global `document`. Give the owner a render adapter with `setRenderAdapter`, or boot the application with a `document`.',
typeof document !== 'undefined'
);

adapter = adapterForDocument(document as unknown as SimpleDocument);
ADAPTER_FOR_OWNER.set(owner, adapter);
}

return adapter;
}
11 changes: 6 additions & 5 deletions packages/@ember/-internals/glimmer/lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isDestroying,
} from '@glimmer/destroyable';
import type {
RenderAdapter,
Bounds,
Environment,
RenderResult as GlimmerRenderResult,
Expand All @@ -30,7 +31,7 @@ import { curry } from '@glimmer/runtime/lib/curried-value';
import { inTransaction } from '@glimmer/runtime/lib/environment';
import { renderMain } from '@glimmer/runtime/lib/render';
import { unwrapTemplate } from './component-managers/unwrap-template';
import type { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface';
import type { SimpleElement, SimpleNode } from '@simple-dom/interface';
import type Component from './component';
import type ClassicComponent from './component';
import { BOUNDS } from './component-managers/curly';
Expand All @@ -39,6 +40,7 @@ import RouterResolver from './router-resolver';
import type { OutletState } from '../../routing/route-managers/outlet-state';
import type { IBuilder, RendererRoot } from './base-renderer';
import { BaseRenderer, errorLoopTransaction } from './base-renderer';
import { getRenderAdapter } from './render-adapter';

export {
type IBuilder,
Expand Down Expand Up @@ -187,26 +189,25 @@ export class Renderer extends BaseRenderer {
let { _viewRegistry } = props;
let owner = getOwner(props);
assert('Renderer is unexpectedly missing an owner', owner);
let document = owner.lookup('service:-document') as SimpleDocument;
let env = owner.lookup('-environment:main') as {
isInteractive: boolean;
hasDOM: boolean;
};
let rootTemplate = owner.lookup(P`template:-root`) as TemplateFactory;
let builder = owner.lookup('service:-dom-builder') as IBuilder;
return new this(owner, document, env, rootTemplate, _viewRegistry, builder);
return new this(owner, getRenderAdapter(owner), env, rootTemplate, _viewRegistry, builder);
}

constructor(
owner: InternalOwner,
document: SimpleDocument,
adapter: RenderAdapter,
env: { isInteractive: boolean; hasDOM: boolean },
rootTemplate: TemplateFactory,
viewRegistry: ViewRegistry,
builder = clientBuilder,
resolver = new RouterResolver()
) {
super(owner, env, document, resolver, builder);
super(owner, env, adapter, resolver, builder);
this._rootTemplate = rootTemplate(owner);
this._viewRegistry = viewRegistry || owner.lookup('-view-registry:main');
}
Expand Down
1 change: 0 additions & 1 deletion packages/@ember/-internals/glimmer/tests/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ export {
Component,
htmlSafe,
SafeString,
DOMChanges,
isHTMLSafe,
} from '@ember/-internals/glimmer';
11 changes: 6 additions & 5 deletions packages/@ember/application/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { assert } from '@ember/debug';
import Router from '@ember/routing/router';
import EventDispatcher from '@ember/-internals/views/lib/system/event_dispatcher';
import type Registry from '@ember/-internals/container/lib/registry';
import type { SimpleElement } from '@simple-dom/interface';
import type { SimpleDocument, SimpleElement } from '@simple-dom/interface';
import { adapterForDocument, setRenderAdapter } from '@ember/-internals/glimmer/lib/render-adapter';

/**
The `ApplicationInstance` encapsulates all of the stateful aspects of a
Expand Down Expand Up @@ -104,6 +105,10 @@ class ApplicationInstance extends EngineInstance {

this.setupRegistry(options);

if (options.document) {
setRenderAdapter(this, adapterForDocument(options.document as unknown as SimpleDocument));
}

let rootElement = options.rootElement ?? this.application.rootElement;
if (typeof rootElement === 'string') {
this.rootElement = options.document ? options.document.querySelector(rootElement) : null;
Expand Down Expand Up @@ -298,10 +303,6 @@ class ApplicationInstance extends EngineInstance {
registry.register('-environment:main', coptions.toEnvironment(), {
instantiate: false,
});
registry.register('service:-document', coptions.document!, {
instantiate: false,
});

super.setupRegistry(registry, coptions);
}
}
Expand Down
10 changes: 8 additions & 2 deletions packages/@ember/application/tests/application_instance_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AbstractTestCase as TestCase,
} from 'internal-test-helpers';
import { getDebugFunction, setDebugFunction } from '@ember/debug';
import { getRenderAdapter } from '@ember/-internals/glimmer/lib/render-adapter';

const originalDebug = getDebugFunction('debug');
const noop = function () {};
Expand Down Expand Up @@ -190,7 +191,6 @@ moduleFor(
P`-bucket-cache:main`,
'-view-registry:main',
'-environment:main',
'service:-document',
'event_dispatcher:main',
];

Expand All @@ -203,6 +203,12 @@ moduleFor(
`Engine and parent app share singleton '${key}'`
);
});

assert.strictEqual(
getRenderAdapter(chatEngineInstance),
getRenderAdapter(appInstance),
'Engine and parent app share the render adapter'
);
});
}

Expand All @@ -217,7 +223,7 @@ moduleFor(

ApplicationInstance.setupRegistry(registry);

assert.equal(registry.resolve('service:-document'), document);
assert.ok(registry.resolve('-environment:main'), 'the registry has the environment');
}
}
);
29 changes: 29 additions & 0 deletions packages/@ember/application/tests/visit_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,35 @@ moduleFor(
});
}

[`@test renders into the \`document\` boot option`](assert) {
this.add('template:index', precompileTemplate('<div class="foo">Hi, Mom!</div>'));

let otherDocument = document.implementation.createHTMLDocument('other');
let rootElement = otherDocument.createElement('div');
otherDocument.body.appendChild(rootElement);

let created = new Set();
let createElement = otherDocument.createElement;
otherDocument.createElement = function (tagName, options) {
let element = createElement.call(this, tagName, options);
created.add(element);
return element;
};

let bootOptions = {
isBrowser: false,
document: otherDocument,
rootElement,
};

return this.visit('/', bootOptions).then(() => {
let rendered = rootElement.querySelector('.foo');

assert.strictEqual(rendered.textContent, 'Hi, Mom!');
assert.true(created.has(rendered), 'the `document` boot option created the element');
});
}

[`@test _renderMode: rehydration`](assert) {
assert.expect(2);

Expand Down
4 changes: 3 additions & 1 deletion packages/@ember/engine/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { type FullName, isFactory } from '@ember/-internals/owner';
import type Engine from '@ember/engine';
import type Application from '@ember/application';
import type { SimpleElement } from '@simple-dom/interface';
import { getRenderAdapter, setRenderAdapter } from '@ember/-internals/glimmer/lib/render-adapter';

export interface BootOptions {
isBrowser?: boolean;
Expand Down Expand Up @@ -254,7 +255,6 @@ class EngineInstance extends EmberObject.extend(
P`-bucket-cache:main`,
'-view-registry:main',
`renderer:-dom`,
'service:-document',
];

if (env['isInteractive']) {
Expand All @@ -266,6 +266,8 @@ class EngineInstance extends EmberObject.extend(
let singleton = parent.lookup(key) as object;
this.register(key, singleton, { instantiate: false });
});

setRenderAdapter(this, getRenderAdapter(parent));
}
}

Expand Down
Loading
Loading