diff --git a/packages/@ember/-internals/glimmer/lib/environment.ts b/packages/@ember/-internals/glimmer/lib/environment.ts index ffa77980ad5..a2a21f849ad 100644 --- a/packages/@ember/-internals/glimmer/lib/environment.ts +++ b/packages/@ember/-internals/glimmer/lib/environment.ts @@ -1,11 +1,10 @@ import { ENV } from '@ember/-internals/environment'; -import { get, set, _getProp, _setProp } from '@ember/-internals/metal'; -import type { InternalOwner } from '@ember/-internals/owner'; +import { _getProp, _setProp, get, set } from '@ember/-internals/metal'; import { getDebugName } from '@ember/-internals/utils'; import { constructStyleDeprecationMessage } from '@ember/-internals/views'; -import { assert, deprecate, warn } from '@ember/debug'; import type { DeprecationOptions } from '@ember/debug'; -import { schedule, _backburner } from '@ember/runloop'; +import { assert, deprecate, warn } from '@ember/debug'; +import { _backburner, schedule } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import setGlobalContext from '@glimmer/global-context'; import type { EnvironmentDelegate } from '@glimmer/runtime'; @@ -131,7 +130,7 @@ const VM_ASSERTION_OVERRIDES: { id: string; message: string }[] = []; export class EmberEnvironmentDelegate implements EnvironmentDelegate { public enableDebugTooling: boolean = ENV._DEBUG_RENDER_TREE; - constructor(public owner: InternalOwner, public isInteractive: boolean) {} + constructor(public owner: object, public isInteractive: boolean) {} onTransactionCommit(): void {} } diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index 1c9c0e44469..be0816ad02c 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -6,23 +6,24 @@ import { guidFor } from '@ember/-internals/utils'; import { getViewElement, getViewId } from '@ember/-internals/views'; import { assert } from '@ember/debug'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; -import { destroy } from '@glimmer/destroyable'; +import { associateDestroyableChild, destroy, isDestroyed } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; import type { Bounds, CompileTimeCompilationContext, + CompileTimeResolver, Cursor, DebugRenderTree, - DynamicScope as GlimmerDynamicScope, ElementBuilder, Environment, + DynamicScope as GlimmerDynamicScope, RenderResult, RuntimeContext, + RuntimeResolver, Template, TemplateFactory, } from '@glimmer/interfaces'; -import { CurriedType } from '@glimmer/vm'; import type { Nullable } from '@ember/-internals/utility-types'; import { programCompilationContext } from '@glimmer/opcode-compiler'; import { artifacts, RuntimeOpImpl } from '@glimmer/program'; @@ -35,22 +36,27 @@ import { DOMChanges, DOMTreeConstruction, inTransaction, + renderComponent as glimmerRenderComponent, renderMain, runtimeContext, } from '@glimmer/runtime'; import { unwrapTemplate } from '@glimmer/util'; import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator'; +import { CurriedType } from '@glimmer/vm'; import type { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface'; import RSVP from 'rsvp'; -import type Component from './component'; +import { hasDOM } from '../../browser-environment'; +import type ClassicComponent from './component'; import { BOUNDS } from './component-managers/curly'; import { createRootOutlet } from './component-managers/outlet'; import { RootComponentDefinition } from './component-managers/root'; import { NodeDOMTreeConstruction } from './dom'; import { EmberEnvironmentDelegate } from './environment'; +import { StrictResolver } from './renderer/strict-resolver'; import ResolverImpl from './resolver'; import type { OutletState } from './utils/outlet'; import OutletView from './views/outlet'; +import { registerDestructor } from '@ember/destroyable'; export type IBuilder = (env: Environment, cursor: Cursor) => ElementBuilder; @@ -120,17 +126,71 @@ function errorLoopTransaction(fn: () => void) { } } -class RootState { +type RootState = ClassicRootState | ComponentRootState; + +class ComponentRootState { + readonly type = 'component'; + + #result: RenderResult | undefined; + #render: () => void; + + constructor( + state: RendererState, + definition: object, + options: { into: Cursor; args?: Record } + ) { + this.#render = errorLoopTransaction(() => { + let iterator = glimmerRenderComponent( + state.runtime, + state.builder(state.env, options.into), + state.compilation, + state.owner, + definition, + options?.args + ); + + let result = (this.#result = iterator.sync()); + + associateDestroyableChild(this, this.#result); + + // override .render function after initial render + this.#render = errorLoopTransaction(() => result.rerender({ alwaysRevalidate: false })); + }); + } + + isFor(_component: ClassicComponent): boolean { + return false; + } + + render(): void { + this.#render(); + } + + destroy(): void { + destroy(this); + } + + get destroyed(): boolean { + return isDestroyed(this); + } + + get result(): RenderResult | undefined { + return this.#result; + } +} + +class ClassicRootState { + readonly type = 'classic'; public id: string; public result: RenderResult | undefined; public destroyed: boolean; public render: () => void; constructor( - public root: Component | OutletView, + public root: ClassicComponent | OutletView, public runtime: RuntimeContext, context: CompileTimeCompilationContext, - owner: InternalOwner, + owner: object, template: Template, self: Reference, parentElement: SimpleElement, @@ -200,18 +260,18 @@ class RootState { } } -const renderers: Renderer[] = []; +const renderers: BaseRenderer[] = []; export function _resetRenderers() { renderers.length = 0; } -function register(renderer: Renderer): void { +function register(renderer: BaseRenderer): void { assert('Cannot register the same renderer twice', renderers.indexOf(renderer) === -1); renderers.push(renderer); } -function deregister(renderer: Renderer): void { +function deregister(renderer: BaseRenderer): void { let index = renderers.indexOf(renderer); assert('Cannot deregister unknown unregistered renderer', index !== -1); renderers.splice(index, 1); @@ -219,7 +279,7 @@ function deregister(renderer: Renderer): void { function loopBegin(): void { for (let renderer of renderers) { - renderer._scheduleRevalidate(); + renderer.rerender(); } } @@ -259,7 +319,7 @@ function resolveRenderPromise() { let loops = 0; function loopEnd() { for (let renderer of renderers) { - if (!renderer._isValid()) { + if (!renderer.isValid()) { if (loops > ENV._RERENDER_LOOP_LIMIT) { loops = 0; // TODO: do something better @@ -281,69 +341,265 @@ interface ViewRegistry { [viewId: string]: unknown; } -export class Renderer { - private _rootTemplate: Template; - private _viewRegistry: ViewRegistry; - private _roots: RootState[]; - private _removedRoots: RootState[]; - private _builder: IBuilder; - private _inRenderTransaction = false; +type Resolver = RuntimeResolver & CompileTimeResolver; + +interface RendererData { + owner: object; + runtime: RuntimeContext; + compilation: CompileTimeCompilationContext; + builder: (env: Environment, cursor: Cursor) => ElementBuilder; + resolver: Resolver; + env: { + isInteractive: boolean; + hasDOM: boolean; + document: SimpleDocument; + }; +} - private _owner: InternalOwner; - private _context: CompileTimeCompilationContext; - private _runtime: RuntimeContext; +export class RendererState { + static create(owner: RendererData, renderer: BaseRenderer): RendererState { + const state = new RendererState(owner, renderer); + associateDestroyableChild(renderer, state); + return state; + } - private _lastRevision = -1; - private _destroyed = false; + readonly #data: RendererData; + #lastRevision = -1; + #inRenderTransaction = false; + #destroyed = false; + #roots: RootState[] = []; + #removedRoots: RootState[] = []; - /** @internal */ - _isInteractive: boolean; + private constructor(data: RendererData, renderer: BaseRenderer) { + this.#data = data; - readonly _runtimeResolver: ResolverImpl; + registerDestructor(this, () => { + this.clearAllRoots(renderer); + }); + } - static create(props: { _viewRegistry: any }): Renderer { - 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; + get debug() { + return { + roots: this.#roots, + inRenderTransaction: this.#inRenderTransaction, + isInteractive: this.#data.env.isInteractive, }; - 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); } - constructor( - owner: InternalOwner, - document: SimpleDocument, - env: { isInteractive: boolean; hasDOM: boolean }, - rootTemplate: TemplateFactory, - viewRegistry: ViewRegistry, - builder = clientBuilder - ) { - this._owner = owner; - this._rootTemplate = rootTemplate(owner); - this._viewRegistry = viewRegistry || owner.lookup('-view-registry:main'); - this._roots = []; - this._removedRoots = []; - this._builder = builder; - this._isInteractive = env.isInteractive; + get roots() { + return this.#roots; + } - // resolver is exposed for tests - let resolver = (this._runtimeResolver = new ResolverImpl()); + get owner(): object { + return this.#data.owner; + } - let sharedArtifacts = artifacts(); + get runtime(): RuntimeContext { + return this.#data.runtime; + } - this._context = programCompilationContext( - sharedArtifacts, - resolver, - (heap) => new RuntimeOpImpl(heap) + get builder(): (env: Environment, cursor: Cursor) => ElementBuilder { + return this.#data.builder; + } + + get compilation(): CompileTimeCompilationContext { + return this.#data.compilation; + } + + get env(): Environment { + return this.runtime.env; + } + + get isInteractive(): boolean { + return this.#data.env.isInteractive; + } + + renderRoot(root: RootState, renderer: BaseRenderer): RootState { + let roots = this.#roots; + + roots.push(root); + associateDestroyableChild(this, root); + + if (roots.length === 1) { + register(renderer); + } + + this.#renderRootsTransaction(renderer); + + return root; + } + + #renderRootsTransaction(renderer: BaseRenderer): void { + if (this.#inRenderTransaction) { + // currently rendering roots, a new root was added and will + // be processed by the existing _renderRoots invocation + return; + } + + // used to prevent calling _renderRoots again (see above) + // while we are actively rendering roots + this.#inRenderTransaction = true; + + let completedWithoutError = false; + try { + this.renderRoots(renderer); + completedWithoutError = true; + } finally { + if (!completedWithoutError) { + this.#lastRevision = valueForTag(CURRENT_TAG); + } + this.#inRenderTransaction = false; + } + } + + renderRoots(renderer: BaseRenderer): void { + let roots = this.#roots; + let removedRoots = this.#removedRoots; + let initialRootsLength: number; + let runtime = this.runtime; + + do { + initialRootsLength = roots.length; + + inTransaction(runtime.env, () => { + // ensure that for the first iteration of the loop + // each root is processed + for (let i = 0; i < roots.length; i++) { + let root = roots[i]; + assert('has root', root); + + if (root.destroyed) { + // add to the list of roots to be removed + // they will be removed from `this._roots` later + removedRoots.push(root); + + // skip over roots that have been marked as destroyed + continue; + } + + // when processing non-initial reflush loops, + // do not process more roots than needed + if (i >= initialRootsLength) { + continue; + } + + root.render(); + } + + this.#lastRevision = valueForTag(CURRENT_TAG); + }); + } while (roots.length > initialRootsLength); + + // remove any roots that were destroyed during this transaction + while (removedRoots.length) { + let root = removedRoots.pop(); + + let rootIndex = roots.indexOf(root!); + roots.splice(rootIndex, 1); + } + + if (this.#roots.length === 0) { + deregister(renderer); + } + } + + scheduleRevalidate(renderer: BaseRenderer): void { + _backburner.scheduleOnce('render', this, this.revalidate, renderer); + } + + isValid(): boolean { + return ( + this.#destroyed || this.#roots.length === 0 || validateTag(CURRENT_TAG, this.#lastRevision) + ); + } + + revalidate(renderer: BaseRenderer): void { + if (this.isValid()) { + return; + } + this.#renderRootsTransaction(renderer); + } + + clearAllRoots(renderer: BaseRenderer): void { + let roots = this.#roots; + for (let root of roots) { + destroy(root); + } + + this.#removedRoots.length = 0; + this.#roots = []; + + // if roots were present before destroying + // deregister this renderer instance + if (roots.length) { + deregister(renderer); + } + } +} + +type IntoTarget = Cursor | Element | SimpleElement; + +function intoTarget(into: IntoTarget): Cursor { + if ('element' in into) { + return into; + } else { + return { element: into as SimpleElement, nextSibling: null }; + } +} + +/** + * This function returns `undefined` if there was an error rendering the + * component. + * + * @fixme restructure this to return a result containing the error rather than + * undefined. + */ +export function renderComponent( + component: object, + { + owner, + env, + into, + args, + }: { + owner: object; + env: { document: SimpleDocument | Document; isInteractive: boolean; hasDOM?: boolean }; + into: IntoTarget; + args?: Record; + } +): RenderResult | undefined { + let renderer = BaseRenderer.strict(owner, env.document, env); + + return renderer.render(component, { into, args }).result; +} + +export class BaseRenderer { + static strict( + owner: object, + document: SimpleDocument | Document, + options: { isInteractive: boolean; hasDOM?: boolean } + ) { + return new BaseRenderer( + owner, + { hasDOM: hasDOM, ...options }, + document as SimpleDocument, + new StrictResolver(), + clientBuilder ); + } + + readonly state: RendererState; + constructor( + owner: object, + env: { isInteractive: boolean; hasDOM: boolean }, + document: SimpleDocument, + resolver: Resolver, + builder: (env: Environment, cursor: Cursor) => ElementBuilder + ) { let runtimeEnvironmentDelegate = new EmberEnvironmentDelegate(owner, env.isInteractive); - this._runtime = runtimeContext( + let sharedArtifacts = artifacts(); + let runtime = runtimeContext( { appendOperations: env.hasDOM ? new DOMTreeConstruction(document) @@ -354,10 +610,26 @@ export class Renderer { sharedArtifacts, resolver ); + + this.state = RendererState.create( + { + owner, + runtime, + builder, + resolver, + compilation: programCompilationContext( + sharedArtifacts, + resolver, + (heap) => new RuntimeOpImpl(heap) + ), + env: { ...env, document: document }, + }, + this + ); } get debugRenderTree(): DebugRenderTree { - let { debugRenderTree } = this._runtime.env; + let { debugRenderTree } = this.state.env; assert( 'Attempted to access the DebugRenderTree, but it did not exist. Is the Ember Inspector open?', @@ -367,6 +639,80 @@ export class Renderer { return debugRenderTree; } + isValid(): boolean { + return this.state.isValid(); + } + + destroy() { + destroy(this); + } + + render( + component: object, + options: { into: IntoTarget; args?: Record } + ): RootState { + const root = new ComponentRootState(this.state, component, { + args: options.args, + into: intoTarget(options.into), + }); + return this.state.renderRoot(root, this); + } + + rerender(): void { + this.state.scheduleRevalidate(this); + } + + // render(component: Component, options: { into: Cursor; args?: Record }): void { + // this.state.renderRoot(component); + // } +} + +export class Renderer extends BaseRenderer { + static strict( + owner: object, + document: SimpleDocument | Document, + options: { isInteractive: boolean; hasDOM?: boolean } + ): BaseRenderer { + return new BaseRenderer( + owner, + { hasDOM: hasDOM, ...options }, + document as SimpleDocument, + new StrictResolver(), + clientBuilder + ); + } + + private _rootTemplate: Template; + private _viewRegistry: ViewRegistry; + + static create(props: { _viewRegistry: any }): Renderer { + 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); + } + + constructor( + owner: InternalOwner, + document: SimpleDocument, + env: { isInteractive: boolean; hasDOM: boolean }, + rootTemplate: TemplateFactory, + viewRegistry: ViewRegistry, + builder = clientBuilder, + resolver = new ResolverImpl() + ) { + super(owner, env, document, resolver, builder); + this._rootTemplate = rootTemplate(owner); + this._viewRegistry = viewRegistry || owner.lookup('-view-registry:main'); + } + // renderer HOOKS appendOutletView(view: OutletView, target: SimpleElement): void { @@ -378,95 +724,99 @@ export class Renderer { ); } - appendTo(view: Component, target: SimpleElement): void { + appendTo(view: ClassicComponent, target: SimpleElement): void { let definition = new RootComponentDefinition(view); this._appendDefinition( view, - curry(CurriedType.Component, definition, this._owner, null, true), + curry(CurriedType.Component, definition, this.state.owner, null, true), target ); } _appendDefinition( - root: OutletView | Component, + root: OutletView | ClassicComponent, definition: CurriedValue, target: SimpleElement ): void { let self = createConstRef(definition, 'this'); let dynamicScope = new DynamicScope(null, UNDEFINED_REFERENCE); - let rootState = new RootState( + let rootState = new ClassicRootState( root, - this._runtime, - this._context, - this._owner, + this.state.runtime, + this.state.compilation, + this.state.owner, this._rootTemplate, self, target, dynamicScope, - this._builder - ); - this._renderRoot(rootState); - } - - rerender(): void { - this._scheduleRevalidate(); - } - - register(view: any): void { - let id = getViewId(view); - assert( - 'Attempted to register a view with an id already in use: ' + id, - !this._viewRegistry[id] + this.state.builder ); - this._viewRegistry[id] = view; + this.state.renderRoot(rootState, this); } - unregister(view: any): void { - delete this._viewRegistry[getViewId(view)]; - } - - remove(view: Component): void { - view._transitionTo('destroying'); - - this.cleanupRootFor(view); - - if (this._isInteractive) { - view.trigger('didDestroyElement'); - } - } - - cleanupRootFor(view: unknown): void { + cleanupRootFor(component: ClassicComponent): void { // no need to cleanup roots if we have already been destroyed - if (this._destroyed) { + if (isDestroyed(this)) { return; } - let roots = this._roots; + let roots = this.state.roots; // traverse in reverse so we can remove items // without mucking up the index - let i = this._roots.length; + let i = roots.length; while (i--) { let root = roots[i]; assert('has root', root); - if (root.isFor(view)) { + if (root.type === 'classic' && root.isFor(component)) { root.destroy(); roots.splice(i, 1); } } } - destroy() { - if (this._destroyed) { - return; + remove(view: ClassicComponent): void { + view._transitionTo('destroying'); + + this.cleanupRootFor(view); + + if (this.state.isInteractive) { + view.trigger('didDestroyElement'); } - this._destroyed = true; - this._clearAllRoots(); } - getElement(view: View): Nullable { + get _roots() { + return this.state.debug.roots; + } + + get _inRenderTransaction() { + return this.state.debug.inRenderTransaction; + } + + get _isInteractive() { + return this.state.debug.isInteractive; + } + + get _context() { + return this.state.compilation; + } + + register(view: any): void { + let id = getViewId(view); + assert( + 'Attempted to register a view with an id already in use: ' + id, + !this._viewRegistry[id] + ); + this._viewRegistry[id] = view; + } + + unregister(view: any): void { + delete this._viewRegistry[getViewId(view)]; + } + + getElement(component: View): Nullable { if (this._isInteractive) { - return getViewElement(view); + return getViewElement(component); } else { throw new Error( 'Accessing `this.element` is not allowed in non-interactive environments (such as FastBoot).' @@ -474,12 +824,12 @@ export class Renderer { } } - getBounds(view: View): { + getBounds(component: View): { parentElement: SimpleElement; firstNode: SimpleNode; lastNode: SimpleNode; } { - let bounds: Bounds | null = view[BOUNDS]; + let bounds: Bounds | null = component[BOUNDS]; assert('object passed to getBounds must have the BOUNDS symbol as a property', bounds); @@ -489,125 +839,4 @@ export class Renderer { return { parentElement, firstNode, lastNode }; } - - createElement(tagName: string): SimpleElement { - return this._runtime.env.getAppendOperations().createElement(tagName); - } - - _renderRoot(root: RootState): void { - let { _roots: roots } = this; - - roots.push(root); - - if (roots.length === 1) { - register(this); - } - - this._renderRootsTransaction(); - } - - _renderRoots(): void { - let { _roots: roots, _runtime: runtime, _removedRoots: removedRoots } = this; - let initialRootsLength: number; - - do { - initialRootsLength = roots.length; - - inTransaction(runtime.env, () => { - // ensure that for the first iteration of the loop - // each root is processed - for (let i = 0; i < roots.length; i++) { - let root = roots[i]; - assert('has root', root); - - if (root.destroyed) { - // add to the list of roots to be removed - // they will be removed from `this._roots` later - removedRoots.push(root); - - // skip over roots that have been marked as destroyed - continue; - } - - // when processing non-initial reflush loops, - // do not process more roots than needed - if (i >= initialRootsLength) { - continue; - } - - root.render(); - } - - this._lastRevision = valueForTag(CURRENT_TAG); - }); - } while (roots.length > initialRootsLength); - - // remove any roots that were destroyed during this transaction - while (removedRoots.length) { - let root = removedRoots.pop(); - - let rootIndex = roots.indexOf(root!); - roots.splice(rootIndex, 1); - } - - if (this._roots.length === 0) { - deregister(this); - } - } - - _renderRootsTransaction(): void { - if (this._inRenderTransaction) { - // currently rendering roots, a new root was added and will - // be processed by the existing _renderRoots invocation - return; - } - - // used to prevent calling _renderRoots again (see above) - // while we are actively rendering roots - this._inRenderTransaction = true; - - let completedWithoutError = false; - try { - this._renderRoots(); - completedWithoutError = true; - } finally { - if (!completedWithoutError) { - this._lastRevision = valueForTag(CURRENT_TAG); - } - this._inRenderTransaction = false; - } - } - - _clearAllRoots(): void { - let roots = this._roots; - for (let root of roots) { - root.destroy(); - } - - this._removedRoots.length = 0; - this._roots = []; - - // if roots were present before destroying - // deregister this renderer instance - if (roots.length) { - deregister(this); - } - } - - _scheduleRevalidate(): void { - _backburner.scheduleOnce('render', this, this._revalidate); - } - - _isValid(): boolean { - return ( - this._destroyed || this._roots.length === 0 || validateTag(CURRENT_TAG, this._lastRevision) - ); - } - - _revalidate(): void { - if (this._isValid()) { - return; - } - this._renderRootsTransaction(); - } } diff --git a/packages/@ember/-internals/glimmer/lib/renderer/strict-resolver.ts b/packages/@ember/-internals/glimmer/lib/renderer/strict-resolver.ts new file mode 100644 index 00000000000..69dc88483f3 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/renderer/strict-resolver.ts @@ -0,0 +1,40 @@ +import type { + CompileTimeResolver as VMCompileTimeResolver, + InternalComponentManager, + Nullable, + ResolvedComponentDefinition, + RuntimeResolver as VMRuntimeResolver, +} from '@glimmer/interfaces'; +import { BUILTIN_HELPERS, BUILTIN_KEYWORD_HELPERS } from '../resolver'; + +/////////// + +/** + * Resolution for non built ins is now handled by the vm as we are using strict mode + */ +export class StrictResolver implements VMRuntimeResolver, VMCompileTimeResolver { + lookupHelper(name: string, _owner: object): Nullable { + return BUILTIN_HELPERS[name] ?? null; + } + + lookupBuiltInHelper(name: string): Nullable { + return BUILTIN_KEYWORD_HELPERS[name] ?? null; + } + + lookupModifier(_name: string, _owner: object): Nullable { + return null; + } + + lookupComponent( + _name: string, + _owner: object + ): Nullable< + ResolvedComponentDefinition> + > { + return null; + } + + lookupBuiltInModifier(_name: string): Nullable { + return null; + } +} diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index 3df207331b0..3017b89f9cc 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -120,7 +120,7 @@ function lookupComponentPair( } } -const BUILTIN_KEYWORD_HELPERS: Record = { +export const BUILTIN_KEYWORD_HELPERS: Record = { action, mut, readonly, @@ -135,7 +135,7 @@ const BUILTIN_KEYWORD_HELPERS: Record = { '-in-el-null': inElementNullCheckHelper, }; -const BUILTIN_HELPERS: Record = { +export const BUILTIN_HELPERS: Record = { ...BUILTIN_KEYWORD_HELPERS, array, concat, diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts new file mode 100644 index 00000000000..73c8b3d7f90 --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts @@ -0,0 +1,284 @@ +import { + AbstractStrictTestCase, + assertClassicComponentElement, + assertHTML, + buildOwner, + clickElement, + defComponent, + defineComponent, + defineSimpleHelper, + defineSimpleModifier, + moduleFor, + type ClassicComponentShape, +} from 'internal-test-helpers'; + +import { Input, Textarea } from '@ember/component'; +import { array, concat, fn, get, hash, on } from '@glimmer/runtime'; +import GlimmerishComponent from '../../utils/glimmerish-component'; + +import { run } from '@ember/runloop'; +import { associateDestroyableChild } from '@glimmer/destroyable'; +import type { RenderResult } from '@glimmer/interfaces'; +import { renderComponent } from '../../../lib/renderer'; + +class RenderComponentTestCase extends AbstractStrictTestCase { + component: RenderResult | undefined; + owner: object; + + constructor(assert: QUnit['assert']) { + super(assert); + + this.owner = buildOwner({}); + associateDestroyableChild(this, this.owner); + } + + get element() { + return document.querySelector('#qunit-fixture')!; + } + + renderComponent( + component: object, + options: { expect: string } | { classic: ClassicComponentShape } + ) { + let { owner } = this; + + run(() => { + this.component = renderComponent(component, { + owner, + env: { document: document, isInteractive: true, hasDOM: true }, + into: this.element, + }); + if (this.component) { + associateDestroyableChild(this, this.component); + } + }); + + if ('expect' in options) { + assertHTML(options.expect); + } else { + assertClassicComponentElement(options.classic); + } + + this.assertStableRerender(); + } +} + +moduleFor( + 'Strict Mode - renderComponent', + class extends RenderComponentTestCase { + '@test Can use a component in scope'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('', { scope: { Foo } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a custom helper in scope (in append position)'() { + let foo = defineSimpleHelper(() => 'Hello, world!'); + let Root = defComponent('{{foo}}', { scope: { foo } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a custom modifier in scope'() { + let foo = defineSimpleModifier((element) => (element.innerHTML = 'Hello, world!')); + let Root = defComponent('
', { scope: { foo } }); + + this.renderComponent(Root, { expect: '
Hello, world!
' }); + } + + '@test Can shadow keywords'() { + let ifComponent = defineComponent({}, 'Hello, world!'); + let Bar = defComponent('{{#if}}{{/if}}', { scope: { if: ifComponent } }); + + this.renderComponent(Bar, { expect: 'Hello, world!' }); + } + + '@test Can use constant values in ambiguous helper/component position'() { + let value = 'Hello, world!'; + + let Root = defComponent('{{value}}', { scope: { value } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use inline if and unless in strict mode templates'() { + let Root = defComponent('{{if true "foo" "bar"}}{{unless true "foo" "bar"}}'); + + this.renderComponent(Root, { expect: 'foobar' }); + } + + '@test Can use a dynamic component definition'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('', { + component: class extends GlimmerishComponent { + Foo = Foo; + }, + }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a dynamic component definition (curly)'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('{{this.Foo}}', { + component: class extends GlimmerishComponent { + Foo = Foo; + }, + }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a dynamic helper definition'() { + let foo = defineSimpleHelper(() => 'Hello, world!'); + let Root = defComponent('{{this.foo}}', { + component: class extends GlimmerishComponent { + foo = foo; + }, + }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a curried dynamic helper'() { + let foo = defineSimpleHelper((value) => value); + let Foo = defComponent('{{@value}}'); + let Root = defComponent('', { + scope: { Foo, foo }, + }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + } + + '@test Can use a curried dynamic modifier'() { + let foo = defineSimpleModifier((element, [text]) => (element.innerHTML = text)); + let Foo = defComponent('
'); + let Root = defComponent('', { + scope: { Foo, foo }, + }); + + this.renderComponent(Root, { expect: '
Hello, world!
' }); + } + } +); + +moduleFor( + 'Strict Mode - renderComponent - built ins', + class extends RenderComponentTestCase { + '@test Can use Input'() { + let Root = defComponent('', { scope: { Input } }); + + this.renderComponent(Root, { + classic: { + tagName: 'input', + attrs: { + type: 'text', + class: 'ember-text-field ember-view', + }, + }, + }); + } + + '@test Can use Textarea'() { + let Root = defComponent('