diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index 6708d17b3bb..de7de6640bb 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -34,7 +34,12 @@ export { PROPERTY_DID_CHANGE, } from './lib/property_events'; export { defineProperty } from './lib/properties'; -export { isElementDescriptor, nativeDescDecorator } from './lib/decorator'; +export { + Decorator, + DecoratorPropertyDescriptor, + isElementDescriptor, + nativeDescDecorator, +} from './lib/decorator'; export { descriptorForDecorator, descriptorForProperty, @@ -54,7 +59,13 @@ export { default as expandProperties } from './lib/expand_properties'; export { addObserver, activateObserver, removeObserver, flushAsyncObservers } from './lib/observer'; 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 { + tagForProperty, + tagFor, + markObjectAsDirty, + UNKNOWN_PROPERTY_TAG, + update, +} from './lib/tags'; export { default as runInTransaction, didRender, assertNotRendered } from './lib/transaction'; export { consume, Tracker, tracked, track } from './lib/tracked'; diff --git a/packages/@ember/object/compat.ts b/packages/@ember/object/compat.ts new file mode 100644 index 00000000000..fe73e661022 --- /dev/null +++ b/packages/@ember/object/compat.ts @@ -0,0 +1,91 @@ +import { Meta } from '@ember/-internals/meta'; +import { + consume, + Decorator, + DecoratorPropertyDescriptor, + isElementDescriptor, + setClassicDecorator, + tagForProperty, + track, + update, +} from '@ember/-internals/metal'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; +import { assert } from '@ember/debug'; + +let wrapGetterSetter = function(_target: object, key: string, desc: PropertyDescriptor) { + let { get: originalGet } = desc; + + if (originalGet !== undefined) { + desc.get = function() { + let propertyTag = tagForProperty(this, key); + let ret; + + let tag = track(() => { + ret = originalGet!.call(this); + }); + + update(propertyTag, tag); + consume(tag); + + return ret; + }; + } + + return desc; +}; + +export function dependentKeyCompat( + target: object, + key: string, + desc: PropertyDescriptor +): PropertyDescriptor; +export function dependentKeyCompat(desc: { get?: Function; set?: Function }): Decorator; +export function dependentKeyCompat( + target: object | { get?: Function; set?: Function }, + key?: string, + desc?: PropertyDescriptor +) { + assert( + 'The dependentKeyCompat decorator can only be used if the tracked properties feature is enabled', + Boolean(EMBER_METAL_TRACKED_PROPERTIES) + ); + + if (!isElementDescriptor([target, key, desc])) { + desc = target as PropertyDescriptor; + + let decorator = function( + target: object, + key: string, + _desc: DecoratorPropertyDescriptor, + _meta?: Meta, + isClassicDecorator?: boolean + ) { + assert( + 'The @dependentKeyCompat decorator may only be passed a method when used in classic classes. You should decorate getters/setters directly in native classes', + isClassicDecorator + ); + + assert( + 'The dependentKeyCompat() decorator must be passed a getter or setter when used in classic classes', + desc !== null && + typeof desc === 'object' && + (typeof desc.get === 'function' || typeof desc.set === 'function') + ); + + return wrapGetterSetter(target, key, desc!); + }; + + setClassicDecorator(decorator); + + return decorator as Decorator; + } + + assert( + 'The @dependentKeyCompat decorator must be applied to getters/setters when used in native classes', + (desc !== null && typeof desc!.get === 'function') || typeof desc!.set === 'function' + ); + + return wrapGetterSetter(target, key!, desc!); +} + +setClassicDecorator(dependentKeyCompat as Decorator); diff --git a/packages/@ember/object/tests/computed/dependent-key-compat-test.js b/packages/@ember/object/tests/computed/dependent-key-compat-test.js new file mode 100644 index 00000000000..f2541b6d30b --- /dev/null +++ b/packages/@ember/object/tests/computed/dependent-key-compat-test.js @@ -0,0 +1,126 @@ +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; + +import { Object as EmberObject } from '@ember/-internals/runtime'; +import { computed, tracked, observer } from '@ember/-internals/metal'; +import { dependentKeyCompat } from '../../compat'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; + +if (EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'dependentKeyCompat', + class extends AbstractTestCase { + '@test it works with computed properties'(assert) { + class Person { + @tracked firstName = 'Tom'; + @tracked lastName = 'Dale'; + + @dependentKeyCompat + get givenName() { + return this.firstName; + } + + @computed('givenName', 'lastName') + get fullName() { + return `${this.givenName} ${this.lastName}`; + } + } + + let tom = new Person(); + + assert.equal(tom.fullName, 'Tom Dale'); + + tom.firstName = 'Thomas'; + + assert.equal(tom.fullName, 'Thomas Dale'); + } + + '@test it works with classic classes'(assert) { + let Person = EmberObject.extend({ + firstName: tracked({ value: 'Tom' }), + lastName: tracked({ value: 'Dale' }), + + givenName: dependentKeyCompat({ + get() { + return this.firstName; + }, + }), + + fullName: computed('givenName', 'lastName', function() { + return `${this.givenName} ${this.lastName}`; + }), + }); + + let tom = Person.create(); + + assert.equal(tom.fullName, 'Tom Dale'); + + tom.firstName = 'Thomas'; + + assert.equal(tom.fullName, 'Thomas Dale'); + } + + async '@test it works with async observers'(assert) { + let count = 0; + + let Person = EmberObject.extend({ + firstName: tracked({ value: 'Tom' }), + lastName: tracked({ value: 'Dale' }), + + givenName: dependentKeyCompat({ + get() { + return this.firstName; + }, + }), + + givenNameObserver: observer({ + dependentKeys: ['givenName'], + fn() { + count++; + }, + sync: false, + }), + }); + + let tom = Person.create(); + + assert.equal(count, 0); + + tom.firstName = 'Thomas'; + await runLoopSettled(); + + assert.equal(count, 1); + } + + '@test it does not work with sync observers'(assert) { + let count = 0; + + let Person = EmberObject.extend({ + firstName: tracked({ value: 'Tom' }), + lastName: tracked({ value: 'Dale' }), + + givenName: dependentKeyCompat({ + get() { + return this.firstName; + }, + }), + + givenNameObserver: observer({ + dependentKeys: ['givenName'], + fn() { + count++; + }, + sync: true, + }), + }); + + let tom = Person.create(); + + assert.equal(count, 0); + + tom.firstName = 'Thomas'; + + assert.equal(count, 0); + } + } + ); +} diff --git a/packages/ember/index.js b/packages/ember/index.js index 43a449d5f1f..ed1c878e7f8 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -29,6 +29,7 @@ import { import Service, { inject as injectService } from '@ember/service'; import { action } from '@ember/object'; +import { dependentKeyCompat } from '@ember/object/compat'; import { and, @@ -456,6 +457,7 @@ Ember.RSVP = RSVP; Ember.Namespace = Namespace; Ember._action = action; +Ember._dependentKeyCompat = dependentKeyCompat; computed.empty = empty; computed.notEmpty = notEmpty; diff --git a/packages/ember/tests/reexports_test.js b/packages/ember/tests/reexports_test.js index 2c7f531a973..cc34700f0b3 100644 --- a/packages/ember/tests/reexports_test.js +++ b/packages/ember/tests/reexports_test.js @@ -1,5 +1,9 @@ import Ember from '../index'; -import { FEATURES, EMBER_NATIVE_DECORATOR_SUPPORT } from '@ember/canary-features'; +import { + FEATURES, + EMBER_NATIVE_DECORATOR_SUPPORT, + EMBER_METAL_TRACKED_PROPERTIES, +} from '@ember/canary-features'; import { confirmExport } from 'internal-test-helpers'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; import { jQueryDisabled, jQuery } from '@ember/-internals/views'; @@ -279,6 +283,9 @@ let allExports = [ { get: 'isNamespaceSearchDisabled', set: 'setNamespaceSearchDisabled' }, ], EMBER_NATIVE_DECORATOR_SUPPORT ? ['_action', '@ember/object', 'action'] : null, + EMBER_METAL_TRACKED_PROPERTIES + ? ['_dependentKeyCompat', '@ember/object/compat', 'dependentKeyCompat'] + : null, ['computed.empty', '@ember/object/computed', 'empty'], ['computed.notEmpty', '@ember/object/computed', 'notEmpty'], ['computed.none', '@ember/object/computed', 'none'],