From 98b4aa9ae4b8dbe50e80201fe40be614bc509260 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Sun, 11 Aug 2019 17:07:03 -0700 Subject: [PATCH] [BUGFIX beta] Autotrack Modifiers and Helpers This PR adds autotracking for modifiers and class based helpers. Modifiers can opt out of autotracking via a new capabilities flag on modifier managers. --- .../glimmer/lib/modifiers/custom.ts | 55 +++++++++++++---- .../glimmer/lib/utils/references.ts | 11 +++- .../custom-modifier-manager-test.js | 60 ++++++++++++++++++- .../tests/integration/helpers/tracked-test.js | 40 +++++++++++++ 4 files changed, 153 insertions(+), 13 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts b/packages/@ember/-internals/glimmer/lib/modifiers/custom.ts index b4154056b6a..1d975e414f2 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 } from '@ember/-internals/metal'; import { Factory } from '@ember/-internals/owner'; +import { assert } 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,24 @@ export interface CustomModifierDefinitionState { delegate: ModifierManagerDelegate; } -export interface Capabilities {} +export interface OptionalCapabilities { + disableLifecycleTracking?: boolean; +} + +export interface Capabilities { + disableLifecycleTracking: boolean; +} // Currently there are no capabilities for modifiers -export function capabilities(_managerAPI: string, _optionalFeatures?: {}): Capabilities { - return {}; +export function capabilities( + managerAPI: '3.13', + optionalFeatures: OptionalCapabilities +): Capabilities { + assert('Invalid modifier manager compatibility specified', managerAPI === '3.13'); + + return { + disableLifecycleTracking: Boolean(optionalFeatures.disableLifecycleTracking), + }; } export class CustomModifierDefinition { @@ -39,6 +54,8 @@ export class CustomModifierDefinition { } export class CustomModifierState { + public tag = createUpdatableTag(); + constructor( public element: Simple.Element, public delegate: ModifierManagerDelegate, @@ -109,18 +126,36 @@ class InteractiveCustomModifierManager return new CustomModifierState(element, definition.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 tracked = track(() => { + delegate.installModifier(modifier, element, args.value()); + }); + + let { capabilities } = delegate; + + if (capabilities === undefined || capabilities.disableLifecycleTracking !== true) { + update(tag, tracked); + } } update(state: CustomModifierState) { - let { args, delegate, modifier } = state; - delegate.updateModifier(modifier, args.value()); + let { args, delegate, modifier, tag } = state; + + let tracked = track(() => { + delegate.updateModifier(modifier, args.value()); + }); + + let { capabilities } = delegate; + + if (capabilities === undefined || capabilities.disableLifecycleTracking !== true) { + update(tag, tracked); + } } 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..e5ce446b121 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/references.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/references.ts @@ -379,16 +379,18 @@ export class ClassBasedHelperReference extends CachedReference { return new ClassBasedHelperReference(instance, args); } + private computeTag = createUpdatableTag(); public tag: Tag; constructor(private instance: HelperInstance, private args: CapturedArguments) { super(); - this.tag = combine([instance[RECOMPUTE_TAG], args.tag]); + this.tag = combine([instance[RECOMPUTE_TAG], args.tag, this.computeTag]); } compute(): Opaque { let { instance, + computeTag, args: { positional, named }, } = this; @@ -400,7 +402,12 @@ export class ClassBasedHelperReference extends CachedReference { debugFreeze(namedValue); } - return instance.compute(positionalValue, namedValue); + let computedValue; + let trackedTag = track(() => (computedValue = instance.compute(positionalValue, namedValue))); + + update(computeTag, trackedTag); + + return computedValue; } } 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..7a1c4654932 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 @@ -2,7 +2,7 @@ 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 { set, tracked } from '@ember/-internals/metal'; class ModifierManagerTest extends RenderingTestCase {} @@ -176,6 +176,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..c3eeb7eb151 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -211,6 +211,46 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { 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'); + } } ); }