diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index c942b1d416d..6697901b6d5 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -306,4 +306,5 @@ export { default as DebugStack } from './lib/utils/debug-stack'; export { default as OutletView } from './lib/views/outlet'; export { capabilities } from './lib/component-managers/custom'; export { setComponentManager, getComponentManager } from './lib/utils/custom-component-manager'; +export { setModifierManager, getModifierManager } from './lib/utils/custom-modifier-manager'; export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers'; diff --git a/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts b/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts new file mode 100644 index 00000000000..b395809dd7c --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts @@ -0,0 +1,109 @@ +import { Factory } from '@ember/-internals/owner'; +import { Dict, Opaque } from '@glimmer/interfaces'; +import { Tag } from '@glimmer/reference'; +import { Arguments, CapturedArguments, ModifierManager } from '@glimmer/runtime'; + +export interface CustomModifierDefinitionState { + ModifierClass: Factory; + name: string; + delegate: ModifierManagerDelegate; +} + +// Currently there are no capabilities for modifiers +export function capabilities() { + return {}; +} + +export class CustomModifierDefinition { + public state: CustomModifierDefinitionState; + public manager = CUSTOM_MODIFIER_MANAGER; + constructor( + public name: string, + ModifierClass: Factory, + public delegate: ModifierManagerDelegate + ) { + this.state = { + ModifierClass, + name, + delegate, + }; + } +} + +export class CustomModifierState { + constructor( + public element: Element, + public delegate: ModifierManagerDelegate, + public modifier: ModifierInstance, + public args: CapturedArguments + ) {} + + destroy() { + const { delegate, modifier, args } = this; + let modifierArgs = valueForCapturedArgs(args); + delegate.destroyModifier(modifier, modifierArgs); + } +} + +export interface CustomModifierManagerArgs { + named: Dict; + positional: Opaque[]; +} + +export interface ModifierManagerDelegate { + createModifier(factory: Opaque, args: CustomModifierManagerArgs): ModifierInstance; + installModifier( + instance: ModifierInstance, + element: Element, + args: CustomModifierManagerArgs + ): void; + updateModifier(instance: ModifierInstance, args: CustomModifierManagerArgs): void; + destroyModifier(instance: ModifierInstance, args: CustomModifierManagerArgs): void; +} + +function valueForCapturedArgs(args: CapturedArguments): CustomModifierManagerArgs { + return { + named: args.named.value(), + positional: args.positional.value(), + }; +} + +class CustomModifierManager + implements + ModifierManager< + CustomModifierState, + CustomModifierDefinitionState + > { + create( + element: Element, + definition: CustomModifierDefinitionState, + args: Arguments + ) { + const capturedArgs = args.capture(); + let modifierArgs = valueForCapturedArgs(capturedArgs); + let instance = definition.delegate.createModifier(definition.ModifierClass, modifierArgs); + return new CustomModifierState(element, definition.delegate, instance, capturedArgs); + } + + getTag({ args }: CustomModifierState): Tag { + return args.tag; + } + + install(state: CustomModifierState) { + let { element, args, delegate, modifier } = state; + let modifierArgs = valueForCapturedArgs(args); + delegate.installModifier(modifier, element, modifierArgs); + } + + update(state: CustomModifierState) { + let { args, delegate, modifier } = state; + let modifierArgs = valueForCapturedArgs(args); + delegate.updateModifier(modifier, modifierArgs); + } + + getDestructor(state: CustomModifierState) { + return state; + } +} + +const CUSTOM_MODIFIER_MANAGER = new CustomModifierManager(); diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index 754c2fc93fc..8df76b995af 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -2,7 +2,11 @@ import { privatize as P } from '@ember/-internals/container'; import { ENV } from '@ember/-internals/environment'; import { LookupOptions, Owner, setOwner } from '@ember/-internals/owner'; import { lookupComponent, lookupPartial, OwnedTemplateMeta } from '@ember/-internals/views'; -import { EMBER_MODULE_UNIFICATION, GLIMMER_CUSTOM_COMPONENT_MANAGER } from '@ember/canary-features'; +import { + EMBER_MODULE_UNIFICATION, + GLIMMER_CUSTOM_COMPONENT_MANAGER, + GLIMMER_MODIFIER_MANAGER, +} from '@ember/canary-features'; import { assert } from '@ember/debug'; import { _instrumentStart } from '@ember/instrumentation'; import { DEBUG } from '@glimmer/env'; @@ -37,11 +41,13 @@ import { default as queryParams } from './helpers/query-param'; import { default as readonly } from './helpers/readonly'; import { default as unbound } from './helpers/unbound'; import ActionModifierManager from './modifiers/action'; +import { CustomModifierDefinition } from './modifiers/custom'; import { populateMacros } from './syntax'; import { mountHelper } from './syntax/mount'; import { outletHelper } from './syntax/outlet'; import { Factory as TemplateFactory, Injections, OwnedTemplate } from './template'; import { getComponentManager } from './utils/custom-component-manager'; +import { getModifierManager } from './utils/custom-modifier-manager'; import { ClassBasedHelperReference, SimpleHelperReference } from './utils/references'; function instrumentationPayload(name: string) { @@ -175,8 +181,8 @@ export default class RuntimeResolver implements IRuntimeResolver { - return this.handle(this._lookupModifier(name)); + lookupModifier(name: string, meta: OwnedTemplateMeta): Option { + return this.handle(this._lookupModifier(name, meta)); } /** @@ -272,8 +278,28 @@ export default class RuntimeResolver implements IRuntimeResolver ModifierManagerDelegate; + +const MANAGERS: WeakMap = new WeakMap(); + +export function setModifierManager(factory: ModifierManagerFactory, obj: any) { + MANAGERS.set(obj, factory); + return obj; +} + +export function getModifierManager(obj: any): undefined | ModifierManagerFactory { + if (!GLIMMER_MODIFIER_MANAGER) { + return; + } + + let pointer = obj; + while (pointer !== undefined && pointer !== null) { + if (MANAGERS.has(pointer)) { + return MANAGERS.get(pointer); + } + + pointer = getPrototypeOf(pointer); + } + + return; +} diff --git a/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js b/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js new file mode 100644 index 00000000000..e01786ede08 --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js @@ -0,0 +1,182 @@ +import { moduleFor, RenderingTest } from '../utils/test-case'; +import { Object as EmberObject } from '@ember/-internals/runtime'; +import { GLIMMER_MODIFIER_MANAGER } from '@ember/canary-features'; +import { setModifierManager } from '@ember/-internals/glimmer'; +import { set } from '@ember/-internals/metal'; + +if (GLIMMER_MODIFIER_MANAGER) { + class ModifierManagerTest extends RenderingTest {} + + class CustomModifierManager { + constructor(owner) { + this.owner = owner; + } + + createModifier(factory, args) { + return factory.create(args); + } + + installModifier(instance, element, args) { + instance.element = element; + let { positional, named } = args; + instance.didInsertElement(positional, named); + } + + updateModifier(instance, args) { + let { positional, named } = args; + instance.didUpdate(positional, named); + } + + destroyModifier(instance) { + instance.willDestroyElement(); + } + } + + moduleFor( + 'Basic Custom Modifier Manager', + class extends ModifierManagerTest { + '@test can register a custom element modifier and render it'(assert) { + let ModifierClass = setModifierManager( + owner => { + return new CustomModifierManager(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + didInsertElement() { + assert.ok(true, 'Called didInsertElement'); + }, + }) + ); + + this.render('

hello world

'); + this.assertHTML(`

hello world

`); + } + + '@test custom lifecycle hooks'(assert) { + assert.expect(9); + let ModifierClass = setModifierManager( + owner => { + return new CustomModifierManager(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + didUpdate([truthy]) { + assert.ok(true, 'Called didUpdate'); + assert.equal(truthy, 'true', 'gets updated args'); + }, + didInsertElement([truthy]) { + assert.ok(true, 'Called didInsertElement'); + assert.equal(truthy, true, 'gets initial args'); + }, + willDestroyElement() { + assert.ok(true, 'Called willDestroyElement'); + }, + }) + ); + + this.render('{{#if truthy}}

hello world

{{/if}}', { + truthy: true, + }); + this.assertHTML(`

hello world

`); + + this.runTask(() => set(this.context, 'truthy', 'true')); + + this.runTask(() => set(this.context, 'truthy', false)); + + this.runTask(() => set(this.context, 'truthy', true)); + } + + '@test associates manager even through an inheritance structure'(assert) { + assert.expect(5); + let ModifierClass = setModifierManager( + owner => { + return new CustomModifierManager(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + ModifierClass = ModifierClass.extend({ + didInsertElement([truthy]) { + this._super(...arguments); + assert.ok(true, 'Called didInsertElement'); + assert.equal(truthy, true, 'gets initial args'); + }, + }); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + didInsertElement([truthy]) { + this._super(...arguments); + assert.ok(true, 'Called didInsertElement'); + assert.equal(truthy, true, 'gets initial args'); + }, + }) + ); + + this.render('

hello world

', { + truthy: true, + }); + this.assertHTML(`

hello world

`); + } + + '@test can give consistent access to underlying DOM element'(assert) { + assert.expect(4); + let ModifierClass = setModifierManager( + owner => { + return new CustomModifierManager(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + savedElement: undefined, + didInsertElement() { + assert.equal(this.element.tagName, 'H1'); + this.set('savedElement', this.element); + }, + didUpdate() { + assert.equal(this.element, this.savedElement); + }, + willDestroyElement() { + assert.equal(this.element, this.savedElement); + }, + }) + ); + + this.render('

hello world

', { + truthy: true, + }); + this.assertHTML(`

hello world

`); + + this.runTask(() => set(this.context, 'truthy', 'true')); + } + } + ); +} diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts index 07627d4a9ed..4a247f167eb 100644 --- a/packages/@ember/canary-features/index.ts +++ b/packages/@ember/canary-features/index.ts @@ -14,6 +14,7 @@ export const DEFAULT_FEATURES = { EMBER_ENGINES_MOUNT_PARAMS: true, EMBER_MODULE_UNIFICATION: null, GLIMMER_CUSTOM_COMPONENT_MANAGER: true, + GLIMMER_MODIFIER_MANAGER: null, EMBER_TEMPLATE_BLOCK_LET_HELPER: true, EMBER_METAL_TRACKED_PROPERTIES: null, EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION: true, @@ -86,3 +87,4 @@ export const EMBER_TEMPLATE_BLOCK_LET_HELPER = featureValue( export const EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION = featureValue( FEATURES.EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION ); +export const GLIMMER_MODIFIER_MANAGER = featureValue(FEATURES.GLIMMER_MODIFIER_MANAGER); diff --git a/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js b/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js index 7fb0a33599f..f32af637b57 100644 --- a/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js +++ b/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js @@ -170,6 +170,12 @@ export default class AbstractRenderingTestCase extends AbstractTestCase { } } + registerModifier(name, ModifierClass) { + let { owner } = this; + + owner.register(`modifier:${name}`, ModifierClass); + } + registerComponentManager(name, manager) { let owner = this.env.owner || this.owner; owner.register(`component-manager:${name}`, manager);