diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index ae138e2e6c4..b6f4d79a6dd 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -393,6 +393,6 @@ 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 { capabilities as modifierCapabilties } from './lib/modifiers/custom'; +export { capabilities as modifierCapabilities } from './lib/modifiers/custom'; export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers'; export { setComponentTemplate, getComponentTemplate } from './lib/utils/component-template'; diff --git a/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts b/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts index b4154056b6a..bec47a083b5 100644 --- a/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts +++ b/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts @@ -1,6 +1,8 @@ +import { track, untrack } from '@ember/-internals/metal'; import { Factory } from '@ember/-internals/owner'; +import { assert, deprecate } from '@ember/debug'; import { Dict, Opaque, Simple } from '@glimmer/interfaces'; -import { CONSTANT_TAG, Tag } from '@glimmer/reference'; +import { combine, CONSTANT_TAG, createUpdatableTag, Tag, update } from '@glimmer/reference'; import { Arguments, CapturedArguments, ModifierManager } from '@glimmer/runtime'; export interface CustomModifierDefinitionState { @@ -9,11 +11,36 @@ export interface CustomModifierDefinitionState { delegate: ModifierManagerDelegate; } -export interface Capabilities {} +export interface OptionalCapabilities { + disableAutoTracking?: boolean; +} + +export interface Capabilities { + disableAutoTracking: boolean; +} + +export function capabilities( + managerAPI: string, + optionalFeatures: OptionalCapabilities = {} +): Capabilities { + if (managerAPI !== '3.13') { + managerAPI = '3.13'; + + deprecate( + 'Modifier manager capabilities now require you to pass a valid version when being generated. Valid versions include: 3.13', + false, + { + until: '3.17.0', + id: 'implicit-modifier-manager-capabilities', + } + ); + } + + assert('Invalid modifier manager compatibility specified', managerAPI === '3.13'); -// Currently there are no capabilities for modifiers -export function capabilities(_managerAPI: string, _optionalFeatures?: {}): Capabilities { - return {}; + return { + disableAutoTracking: Boolean(optionalFeatures.disableAutoTracking), + }; } export class CustomModifierDefinition { @@ -39,6 +66,8 @@ export class CustomModifierDefinition { } export class CustomModifierState { + public tag = createUpdatableTag(); + constructor( public element: Simple.Element, public delegate: ModifierManagerDelegate, @@ -101,26 +130,55 @@ class InteractiveCustomModifierManager definition: CustomModifierDefinitionState, args: Arguments ) { + let { delegate, ModifierClass } = definition; const capturedArgs = args.capture(); - let instance = definition.delegate.createModifier( - definition.ModifierClass, - capturedArgs.value() - ); - return new CustomModifierState(element, definition.delegate, instance, capturedArgs); + + let instance = definition.delegate.createModifier(ModifierClass, capturedArgs.value()); + + if (delegate.capabilities === undefined) { + delegate.capabilities = capabilities('3.13'); + + deprecate( + 'Custom modifier managers must define their capabilities using the capabilities() helper function', + false, + { + until: '3.17.0', + id: 'implicit-modifier-manager-capabilities', + } + ); + } + + return new CustomModifierState(element, delegate, instance, capturedArgs); } - getTag({ args }: CustomModifierState): Tag { - return args.tag; + getTag({ args, tag }: CustomModifierState): Tag { + return combine([tag, args.tag]); } install(state: CustomModifierState) { - let { element, args, delegate, modifier } = state; - delegate.installModifier(modifier, element, args.value()); + let { element, args, delegate, modifier, tag } = state; + let { capabilities } = delegate; + + if (capabilities.disableAutoTracking === true) { + untrack(() => delegate.installModifier(modifier, element, args.value())); + } else { + let combinedTrackingTag = track(() => + delegate.installModifier(modifier, element, args.value()) + ); + update(tag, combinedTrackingTag); + } } update(state: CustomModifierState) { - let { args, delegate, modifier } = state; - delegate.updateModifier(modifier, args.value()); + let { args, delegate, modifier, tag } = state; + let { capabilities } = delegate; + + if (capabilities.disableAutoTracking === true) { + untrack(() => delegate.updateModifier(modifier, args.value())); + } else { + let combinedTrackingTag = track(() => delegate.updateModifier(modifier, args.value())); + update(tag, combinedTrackingTag); + } } getDestructor(state: CustomModifierState) { diff --git a/packages/@ember/-internals/glimmer/lib/utils/references.ts b/packages/@ember/-internals/glimmer/lib/utils/references.ts index a1a84507dbf..3388de34a8f 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/references.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/references.ts @@ -349,16 +349,20 @@ export class SimpleHelperReference extends CachedReference { } } + private computeTag: UpdatableTag; public tag: Tag; constructor(private helper: HelperFunction, private args: CapturedArguments) { super(); - this.tag = args.tag; + + let computeTag = (this.computeTag = createUpdatableTag()); + this.tag = combine([args.tag, computeTag]); } compute(): Opaque { let { helper, + computeTag, args: { positional, named }, } = this; @@ -370,7 +374,12 @@ export class SimpleHelperReference extends CachedReference { debugFreeze(namedValue); } - return helper(positionalValue, namedValue); + let computedValue; + let combinedTrackingTag = track(() => (computedValue = helper(positionalValue, namedValue))); + + update(computeTag, combinedTrackingTag); + + return computedValue; } } @@ -379,16 +388,20 @@ export class ClassBasedHelperReference extends CachedReference { return new ClassBasedHelperReference(instance, args); } + private computeTag: UpdatableTag; public tag: Tag; constructor(private instance: HelperInstance, private args: CapturedArguments) { super(); - this.tag = combine([instance[RECOMPUTE_TAG], args.tag]); + + let computeTag = (this.computeTag = createUpdatableTag()); + this.tag = combine([instance[RECOMPUTE_TAG], args.tag, computeTag]); } compute(): Opaque { let { instance, + computeTag, args: { positional, named }, } = this; @@ -400,7 +413,14 @@ export class ClassBasedHelperReference extends CachedReference { debugFreeze(namedValue); } - return instance.compute(positionalValue, namedValue); + let computedValue; + let combinedTrackingTag = track( + () => (computedValue = instance.compute(positionalValue, namedValue)) + ); + + update(computeTag, combinedTrackingTag); + + return computedValue; } } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js index 179c537c2b3..037f54da93e 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js @@ -1,6 +1,6 @@ import { moduleFor, RenderingTestCase, strip, classes, runTask } from 'internal-test-helpers'; import { ENV } from '@ember/-internals/environment'; -import { setModifierManager } from '@ember/-internals/glimmer'; +import { setModifierManager, modifierCapabilities } from '@ember/-internals/glimmer'; import { Object as EmberObject } from '@ember/-internals/runtime'; import { set, setProperties } from '@ember/-internals/metal'; @@ -9,6 +9,7 @@ import { Component } from '../../utils/helpers'; class CustomModifierManager { constructor(owner) { + this.capabilities = modifierCapabilities('3.13'); this.owner = owner; } 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 index 36c2ac04b8e..5d41067675d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js @@ -1,13 +1,14 @@ import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; import { Object as EmberObject } from '@ember/-internals/runtime'; -import { setModifierManager } from '@ember/-internals/glimmer'; -import { set } from '@ember/-internals/metal'; +import { setModifierManager, modifierCapabilities } from '@ember/-internals/glimmer'; +import { set, tracked } from '@ember/-internals/metal'; class ModifierManagerTest extends RenderingTestCase {} class CustomModifierManager { constructor(owner) { + this.capabilities = modifierCapabilities('3.13'); this.owner = owner; } @@ -101,6 +102,49 @@ moduleFor( runTask(() => set(this.context, 'truthy', true)); } + '@test requires modifier capabilities'() { + class WithoutCapabilities extends CustomModifierManager { + constructor(owner) { + super(owner); + this.capabilities = undefined; + } + } + + let ModifierClass = setModifierManager( + owner => { + return new WithoutCapabilities(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + didUpdate() {}, + didInsertElement() {}, + willDestroyElement() {}, + }) + ); + + expectDeprecation(() => { + this.render('

hello world

'); + }, /Custom modifier managers must define their capabilities/); + } + + '@test modifier capabilities require a version'() { + expectDeprecation(() => { + modifierCapabilities(); + }, /Modifier manager capabilities now require you to pass a valid version when being generated/); + + expectDeprecation(() => { + modifierCapabilities('aoeu'); + }, /Modifier manager capabilities now require you to pass a valid version when being generated/); + } + '@test associates manager even through an inheritance structure'(assert) { assert.expect(5); let ModifierClass = setModifierManager( @@ -176,6 +220,64 @@ moduleFor( runTask(() => set(this.context, 'truthy', 'true')); } + + '@test lifecycle hooks are autotracked by default'(assert) { + let TrackedClass = EmberObject.extend({ + count: tracked({ value: 0 }), + }); + + let trackedOne = TrackedClass.create(); + let trackedTwo = TrackedClass.create(); + + let insertCount = 0; + let updateCount = 0; + + let ModifierClass = setModifierManager( + owner => { + return new CustomModifierManager(owner); + }, + EmberObject.extend({ + didInsertElement() {}, + didUpdate() {}, + willDestroyElement() {}, + }) + ); + + this.registerModifier( + 'foo-bar', + ModifierClass.extend({ + didInsertElement() { + // track the count of the first item + trackedOne.count; + insertCount++; + }, + + didUpdate() { + // track the count of the second item + trackedTwo.count; + updateCount++; + }, + }) + ); + + this.render('

hello world

'); + this.assertHTML(`

hello world

`); + + assert.equal(insertCount, 1); + assert.equal(updateCount, 0); + + runTask(() => trackedTwo.count++); + assert.equal(updateCount, 0); + + runTask(() => trackedOne.count++); + assert.equal(updateCount, 1); + + runTask(() => trackedOne.count++); + assert.equal(updateCount, 1); + + runTask(() => trackedTwo.count++); + assert.equal(updateCount, 2); + } } ); diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index 195103a79ee..2774218d5b0 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,6 +1,7 @@ import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { Object as EmberObject, A } from '@ember/-internals/runtime'; import { tracked, nativeDescDecorator as descriptor } from '@ember/-internals/metal'; +import Service, { inject } from '@ember/service'; import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; import { Component } from '../../utils/helpers'; @@ -211,6 +212,98 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { assert.strictEqual(computeCount, 2, 'compute is called exactly 2 times'); } + + '@test functional helpers autotrack based on non-argument tracked props that are accessed'( + assert + ) { + let computeCount = 0; + + let currentUserService; + this.registerService( + 'current-user', + Service.extend({ + name: tracked({ value: 'bob' }), + + init() { + this._super(...arguments); + currentUserService = this; + }, + }) + ); + + this.registerComponent('person', { + ComponentClass: Component.extend({ + currentUser: inject('current-user'), + }), + + template: strip` + {{hello-world this.currentUser}} + `, + }); + + this.registerHelper('hello-world', ([service]) => { + computeCount++; + return `${service.name}-value`; + }); + + this.render(''); + + this.assertText('bob-value'); + + assert.strictEqual(computeCount, 1, 'compute is called exactly 1 time'); + + runTask(() => this.rerender()); + + this.assertText('bob-value'); + + assert.strictEqual(computeCount, 1, 'compute is called exactly 1 time'); + + runTask(() => (currentUserService.name = 'sal')); + + this.assertText('sal-value'); + + assert.strictEqual(computeCount, 2, 'compute is called exactly 2 times'); + } + + '@test class based helpers are autotracked'(assert) { + let computeCount = 0; + + let TrackedClass = EmberObject.extend({ + value: tracked({ value: 'bob' }), + }); + + let trackedInstance = TrackedClass.create(); + + this.registerComponent('person', { + ComponentClass: Component.extend(), + template: strip`{{hello-world}}`, + }); + + this.registerHelper('hello-world', { + compute() { + computeCount++; + return `${trackedInstance.value}-value`; + }, + }); + + this.render(''); + + this.assertText('bob-value'); + + assert.strictEqual(computeCount, 1, 'compute is called exactly 1 time'); + + runTask(() => this.rerender()); + + this.assertText('bob-value'); + + assert.strictEqual(computeCount, 1, 'compute is called exactly 1 time'); + + runTask(() => (trackedInstance.value = 'sal')); + + this.assertText('sal-value'); + + assert.strictEqual(computeCount, 2, 'compute is called exactly 2 times'); + } } ); } diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index a8f863741a8..f0047cc1bc4 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -61,7 +61,7 @@ export { Mixin, aliasMethod, mixin, observer, applyMixin } from './lib/mixin'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; export { tagForProperty, tagFor, markObjectAsDirty, UNKNOWN_PROPERTY_TAG } from './lib/tags'; export { default as runInTransaction, didRender, assertNotRendered } from './lib/transaction'; -export { consume, Tracker, tracked, track } from './lib/tracked'; +export { consume, Tracker, tracked, track, untrack } from './lib/tracked'; export { NAMESPACES, diff --git a/packages/@ember/modifier/index.ts b/packages/@ember/modifier/index.ts index 795f5b60cfe..27aefcd98c5 100644 --- a/packages/@ember/modifier/index.ts +++ b/packages/@ember/modifier/index.ts @@ -1 +1 @@ -export { setModifierManager, modifierCapabilties as capabilties } from '@ember/-internals/glimmer'; +export { setModifierManager, modifierCapabilities as capabilties } from '@ember/-internals/glimmer'; diff --git a/packages/ember/index.js b/packages/ember/index.js index 5d570d55680..ea571f5c3c5 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -112,7 +112,7 @@ import { TextArea, isSerializationFirstNode, setModifierManager, - modifierCapabilties, + modifierCapabilities, setComponentTemplate, getComponentTemplate, } from '@ember/-internals/glimmer'; @@ -528,7 +528,7 @@ Ember.LinkComponent = LinkComponent; Ember._setComponentManager = setComponentManager; Ember._componentManagerCapabilities = capabilities; Ember._setModifierManager = setModifierManager; -Ember._modifierManagerCapabilties = modifierCapabilties; +Ember._modifierManagerCapabilities = modifierCapabilities; if (EMBER_GLIMMER_SET_COMPONENT_TEMPLATE) { Ember._getComponentTemplate = getComponentTemplate; Ember._setComponentTemplate = setComponentTemplate;