diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index 1b9e350a3f9..f219136def1 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -49,7 +49,7 @@ export { Mixin, aliasMethod, mixin, observer, applyMixin } from './lib/mixin'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; export { setHasViews, tagForProperty, tagFor, markObjectAsDirty } from './lib/tags'; export { default as runInTransaction, didRender, assertNotRendered } from './lib/transaction'; -export { tracked } from './lib/tracked'; +export { tracked, getCurrentTracker, setCurrentTracker } from './lib/tracked'; export { NAMESPACES, diff --git a/packages/@ember/-internals/metal/lib/decorator.ts b/packages/@ember/-internals/metal/lib/decorator.ts index 0c399039596..c4fcdad18e9 100644 --- a/packages/@ember/-internals/metal/lib/decorator.ts +++ b/packages/@ember/-internals/metal/lib/decorator.ts @@ -4,7 +4,13 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { unwatch, watch } from './watching'; -const DECORATOR_DESCRIPTOR_MAP: WeakMap = new WeakMap(); +// This needs to be a variable binding due to Rollup, the way it orders module +// declarations it ends up getting _used_ before it's evaluated. Needs to be +// hoisted. +// +// TODO: Figure out a way to make Rollup resolve this correctly +// tslint:disable-next-line no-var-keyword +var DECORATOR_DESCRIPTOR_MAP: WeakMap; // https://tc39.github.io/proposal-decorators/#sec-elementdescriptor-specification-type export interface ElementDescriptor { @@ -72,8 +78,11 @@ export function isComputedDecorator(dec: Decorator | null | undefined) { @param {function} decorator the value to mark as a decorator @private */ -export function setComputedDecorator(dec: Decorator) { - DECORATOR_DESCRIPTOR_MAP.set(dec, true); +export function setComputedDecorator(dec: Decorator, value: any = true) { + if (DECORATOR_DESCRIPTOR_MAP === undefined) { + DECORATOR_DESCRIPTOR_MAP = new WeakMap(); + } + DECORATOR_DESCRIPTOR_MAP.set(dec, value); } // .......................................................... @@ -212,7 +221,7 @@ export function makeComputedDecorator( assert( 'Native decorators are not enabled without the EMBER_NATIVE_DECORATOR_SUPPORT flag', - EMBER_NATIVE_DECORATOR_SUPPORT ? !isClassicDecorator : isClassicDecorator + EMBER_NATIVE_DECORATOR_SUPPORT || isClassicDecorator ); elementDesc.kind = 'method'; @@ -232,7 +241,7 @@ export function makeComputedDecorator( return elementDesc; }; - DECORATOR_DESCRIPTOR_MAP.set(decorator, desc); + setComputedDecorator(decorator, desc); Object.setPrototypeOf(decorator, DecoratorClass.prototype); diff --git a/packages/@ember/-internals/metal/lib/properties.ts b/packages/@ember/-internals/metal/lib/properties.ts index c20febff765..c233a0d5944 100644 --- a/packages/@ember/-internals/metal/lib/properties.ts +++ b/packages/@ember/-internals/metal/lib/properties.ts @@ -155,16 +155,19 @@ export function defineProperty( let value; if (isComputedDecorator(desc)) { - let elementDesc: ElementDescriptor = { + let elementDesc = { key: keyName, kind: 'field', placement: 'own', descriptor: { value: undefined, }, - }; + toString() { + return '[object Descriptor]'; + }, + } as ElementDescriptor; - if (DEBUG && !EMBER_NATIVE_DECORATOR_SUPPORT) { + if (DEBUG) { elementDesc = desc!(elementDesc, true); } else { elementDesc = desc!(elementDesc); diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 3d21d164eb1..f3104c3071d 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -1,14 +1,21 @@ +import { assert } from '@ember/debug'; import { combine, CONSTANT_TAG, Tag } from '@glimmer/reference'; -import { dirty, tagFor, tagForProperty, update } from './tags'; +import { + Decorator, + ElementDescriptor, + setComputedDecorator, + nativeDescDecorator, +} from './decorator'; +import { dirty, tagFor, tagForProperty } from './tags'; +import { DEBUG } from '@glimmer/env'; type Option = T | null; -type unusable = null | undefined | void | {}; /** An object that that tracks @tracked properties that were consumed. @private - */ +*/ class Tracker { private tags = new Set(); private last: Option = null; @@ -95,24 +102,98 @@ class Tracker { ``` @param dependencies Optional dependents to be tracked. - */ -export function tracked(...dependencies: string[]): MethodDecorator; -export function tracked(target: unknown, key: PropertyKey): any; -export function tracked( - target: unknown, - key: PropertyKey, - descriptor: PropertyDescriptor -): PropertyDescriptor; -export function tracked(...dependencies: any[]): any { - let [, key, descriptor] = dependencies; - - if (descriptor === undefined || 'initializer' in descriptor) { - return descriptorForDataProperty(key, descriptor); - } else { - return descriptorForAccessor(key, descriptor); +*/ +export function tracked(propertyDesc: { value: any }): Decorator; +export function tracked(elementDesc: ElementDescriptor): ElementDescriptor; +export function tracked(elementDesc: ElementDescriptor | any): ElementDescriptor | Decorator { + if ( + elementDesc === undefined || + elementDesc === null || + elementDesc.toString() !== '[object Descriptor]' + ) { + assert( + `tracked() may only receive an options object containing 'value', received ${elementDesc}`, + elementDesc === undefined || (elementDesc !== null && typeof elementDesc === 'object') + ); + + if (DEBUG && elementDesc) { + let keys = Object.keys(elementDesc); + let isValue = keys.includes('value'); + let isGetterSetter = keys.includes('get') || keys.includes('set'); + + assert( + `The options object passed to tracked() must be a property descriptor with either 'value' OR 'get'/'set', not both. Received: [${keys}]`, + (keys.length === 1 || keys.length === 2) && (isValue ? !isGetterSetter : isGetterSetter) + ); + } + + if (elementDesc === undefined || 'value' in elementDesc) { + let value = elementDesc ? elementDesc.value : undefined; + + let decorator = function(elementDesc: ElementDescriptor, isClassicDecorator?: boolean) { + assert( + `You attempted to set a default value for ${ + elementDesc.key + } with the @tracked({ value: 'default' }) syntax. You can only use this syntax with classic classes. For native classes, you can use class initializers: @tracked field = 'default';`, + isClassicDecorator + ); + + elementDesc.initializer = () => value; + + return descriptorForField(elementDesc); + }; + + setComputedDecorator(decorator); + + return decorator; + } else { + return nativeDescDecorator(elementDesc); + } } + + return descriptorForField(elementDesc); } +function descriptorForField(elementDesc: ElementDescriptor): ElementDescriptor { + let { key, kind, initializer } = elementDesc as ElementDescriptor; + + assert( + `You attempted to use @tracked on ${key}, but that element is not a class field. @tracked is only usable on class fields. Native getters and setters will autotrack add any tracked fields they encounter, so there is no need mark getters and setters with @tracked.`, + kind === 'field' + ); + + let shadowKey = Symbol(key); + + return { + key, + kind: 'method', + placement: 'prototype', + descriptor: { + enumerable: true, + configurable: true, + + get(): any { + if (CURRENT_TRACKER) CURRENT_TRACKER.add(tagForProperty(this, key)); + + if (!(shadowKey in this)) { + this[shadowKey] = initializer !== undefined ? initializer.call(this) : undefined; + } + + return this[shadowKey]; + }, + + set(newValue: any): void { + tagFor(this).inner!['dirty'](); + dirty(tagForProperty(this, key)); + this[shadowKey] = newValue; + propertyDidChange(); + }, + }, + }; +} + +setComputedDecorator(tracked); + /** @private @@ -127,7 +208,7 @@ export function tracked(...dependencies: any[]): any { The consequence is that each tracked computed property has a tag that corresponds to the tracked properties consumed inside of itself, including child tracked computed properties. - */ +*/ let CURRENT_TRACKER: Option = null; export function getCurrentTracker(): Option { @@ -138,91 +219,8 @@ export function setCurrentTracker(tracker: Tracker = new Tracker()): Tracker { return (CURRENT_TRACKER = tracker); } -function descriptorForAccessor( - key: string | symbol, - descriptor: PropertyDescriptor -): PropertyDescriptor { - let get = descriptor.get as Function; - let set = descriptor.set as Function; - - function getter(this: any): any { - // Swap the parent tracker for a new tracker - let old = CURRENT_TRACKER; - let tracker = (CURRENT_TRACKER = new Tracker()); - - // Call the getter - let ret = get.call(this); - - // Swap back the parent tracker - CURRENT_TRACKER = old; - - // Combine the tags in the new tracker and add them to the parent tracker - let tag = tracker.combine(); - if (CURRENT_TRACKER) CURRENT_TRACKER.add(tag); - - // Update the UpdatableTag for this property with the tag for all of the - // consumed dependencies. - update(tagForProperty(this, key), tag); - - return ret; - } - - function setter(this: unusable): void { - dirty(tagForProperty(this, key)); - set.apply(this, arguments); - } - - return { - enumerable: true, - configurable: false, - get: get && getter, - set: set && setter, - }; -} - export type Key = string; -/** - @private - - A getter/setter for change tracking for a particular key. The accessor - acts just like a normal property, but it triggers the `propertyDidChange` - hook when written to. - - Values are saved on the object using a "shadow key," or a symbol based on the - tracked property name. Sets write the value to the shadow key, and gets read - from it. - */ - -function descriptorForDataProperty( - key: string, - descriptor: PropertyDescriptor -): PropertyDescriptor { - let shadowKey = Symbol(key); - - return { - enumerable: true, - configurable: true, - - get(): any { - if (CURRENT_TRACKER) CURRENT_TRACKER.add(tagForProperty(this, key)); - - if (!(shadowKey in this)) { - this[shadowKey] = descriptor.value; - } - - return this[shadowKey]; - }, - - set(newValue: any): void { - tagFor(this).inner!['dirty'](); - dirty(tagForProperty(this, key)); - this[shadowKey] = newValue; - propertyDidChange(); - }, - }; -} - export interface Interceptors { [key: string]: boolean; } diff --git a/packages/@ember/-internals/metal/tests/accessors/set_test.js b/packages/@ember/-internals/metal/tests/accessors/set_test.js index d159de71c6c..b02020f1893 100644 --- a/packages/@ember/-internals/metal/tests/accessors/set_test.js +++ b/packages/@ember/-internals/metal/tests/accessors/set_test.js @@ -124,5 +124,28 @@ moduleFor( trySet(obj, 'favoriteFood', 'hot dogs'); assert.equal(obj.favoriteFood, undefined, 'does not set and does not error'); } + + ['@test should work with native setters'](assert) { + let count = 0; + + class Foo { + __foo = ''; + + get foo() { + return this.__foo; + } + + set foo(value) { + count++; + this.__foo = `computed ${value}`; + } + } + + let obj = new Foo(); + + assert.equal(set(obj, 'foo', 'bar'), 'bar', 'should return set value'); + assert.equal(count, 1, 'should have native setter'); + assert.equal(get(obj, 'foo'), 'computed bar', 'should return new value'); + } } ); diff --git a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js new file mode 100644 index 00000000000..42c67b36338 --- /dev/null +++ b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js @@ -0,0 +1,169 @@ +import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; +import { defineProperty, tracked } from '../..'; + +import { track } from './support'; + +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; + +if (EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + '@tracked decorator - classic classes', + class extends AbstractTestCase { + [`@test validators for tracked getters with dependencies should invalidate when the dependencies invalidate`]( + assert + ) { + let obj = { + get full() { + return `${this.first} ${this.last}`; + }, + }; + + defineProperty(obj, 'first', tracked); + defineProperty(obj, 'last', tracked()); + + obj.first = 'Tom'; + obj.last = 'Dale'; + + let tag = track(() => obj.full); + let snapshot = tag.value(); + + assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); + assert.equal(tag.validate(snapshot), true); + + snapshot = tag.value(); + assert.equal(tag.validate(snapshot), true); + + obj.first = 'Thomas'; + + assert.equal(tag.validate(snapshot), false); + + assert.equal(obj.full, 'Thomas Dale'); + snapshot = tag.value(); + + assert.equal(tag.validate(snapshot), true); + } + + [`@test tracked can be used to define getters/setters on classic classes`](assert) { + let obj = {}; + + defineProperty(obj, 'first', tracked); + defineProperty(obj, 'last', tracked()); + + defineProperty(obj, 'full', { + get() { + return `${this.first} ${this.last}`; + }, + + set(value) { + let [first, last] = value.split(' '); + + this.first = first; + this.last = last; + }, + }); + + obj.first = 'Tom'; + obj.last = 'Dale'; + + let tag = track(() => obj.full); + let snapshot = tag.value(); + + assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); + assert.equal(tag.validate(snapshot), true); + + snapshot = tag.value(); + assert.equal(tag.validate(snapshot), true); + + obj.full = 'Melanie Sumner'; + + assert.equal(tag.validate(snapshot), false); + + assert.equal(obj.full, 'Melanie Sumner'); + assert.equal(obj.first, 'Melanie'); + assert.equal(obj.last, 'Sumner'); + snapshot = tag.value(); + + assert.equal(tag.validate(snapshot), true); + } + + [`@test can pass a default value to the tracked decorator`](assert) { + class Tracked { + get full() { + return `${this.first} ${this.last}`; + } + } + + defineProperty(Tracked.prototype, 'first', tracked({ value: 'Tom' })); + defineProperty(Tracked.prototype, 'last', tracked({ value: 'Dale' })); + + let obj = new Tracked(); + + assert.equal(obj.full, 'Tom Dale', 'Default values are correctly assign'); + } + + [`@test errors on any keys besides 'value', 'get', or 'set' being passed`]() { + expectAssertion(() => { + class Tracked { + get full() { + return `${this.first} ${this.last}`; + } + } + + defineProperty( + Tracked.prototype, + 'first', + tracked({ + foo() {}, + }) + ); + }, /The options object passed to tracked\(\) must be a property descriptor with either 'value' OR 'get'\/'set'/); + } + + [`@test errors if 'value' and 'get'/'set' are passed together`]() { + expectAssertion(() => { + class Tracked { + get full() { + return `${this.first} ${this.last}`; + } + } + + defineProperty( + Tracked.prototype, + 'first', + tracked({ + value: 123, + get() {}, + set() {}, + }) + ); + }, /The options object passed to tracked\(\) must be a property descriptor with either 'value' OR 'get'\/'set', not both. Received: \[value,get,set\]/); + } + + [`@test errors on anything besides an options object being passed`]() { + expectAssertion(() => { + class Tracked { + get full() { + return `${this.first} ${this.last}`; + } + } + + defineProperty(Tracked.prototype, 'first', tracked(null)); + }, /tracked\(\) may only receive an options object containing 'value', received null/); + } + + [`@test errors options are passed to native decorator`]() { + expectAssertion(() => { + class Tracked { + @tracked() first; + + get full() { + return `${this.first} ${this.last}`; + } + } + + new Tracked(); + }, /You attempted to set a default value for first with the @tracked\({ value: 'default' }\) syntax. You can only use this syntax with classic classes./); + } + } + ); +} diff --git a/packages/@ember/-internals/metal/tests/tracked/computed_test.ts b/packages/@ember/-internals/metal/tests/tracked/computed_test.ts deleted file mode 100644 index 88172798da0..00000000000 --- a/packages/@ember/-internals/metal/tests/tracked/computed_test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { get, set, tracked } from '../..'; - -import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; -import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; - -if (EMBER_METAL_TRACKED_PROPERTIES) { - moduleFor( - '@tracked getters', - class extends AbstractTestCase { - ['@test works without get'](assert: Assert) { - let count = 0; - - class Count { - @tracked - get foo() { - count++; - return `computed foo`; - } - } - - let obj = new Count(); - - assert.equal(obj.foo, 'computed foo', 'should return value'); - assert.equal(count, 1, 'should have invoked computed property'); - } - - ['@test defining computed property should invoke property on get'](assert: Assert) { - let count = 0; - - class Count { - @tracked - get foo() { - count++; - return `computed foo`; - } - } - - let obj = new Count(); - - assert.equal(get(obj, 'foo'), 'computed foo', 'should return value'); - assert.equal(count, 1, 'should have invoked computed property'); - } - - ['@test defining computed property should invoke property on set'](assert: Assert) { - let count = 0; - - class Foo { - __foo = ''; - - @tracked - get foo() { - return this.__foo; - } - - set foo(value) { - count++; - this.__foo = `computed ${value}`; - } - } - - let obj = new Foo(); - - assert.equal(set(obj, 'foo', 'bar'), 'bar', 'should return set value'); - assert.equal(count, 1, 'should have invoked computed property'); - assert.equal(get(obj, 'foo'), 'computed bar', 'should return new value'); - } - } - ); -} diff --git a/packages/@ember/-internals/metal/tests/tracked/get_test.ts b/packages/@ember/-internals/metal/tests/tracked/get_test.js similarity index 79% rename from packages/@ember/-internals/metal/tests/tracked/get_test.ts rename to packages/@ember/-internals/metal/tests/tracked/get_test.js index 250f6dc9c75..96198c01679 100644 --- a/packages/@ember/-internals/metal/tests/tracked/get_test.ts +++ b/packages/@ember/-internals/metal/tests/tracked/get_test.js @@ -3,7 +3,7 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { get, getWithDefault, tracked } from '../..'; if (EMBER_METAL_TRACKED_PROPERTIES) { - const createObj = function() { + let createObj = function() { class Obj { @tracked string = 'string'; @tracked number = 23; @@ -63,23 +63,6 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { this.assert.equal(get(obj, 'path.key.value'), 'value'); } - - '@test should not access a property more than once'() { - let count = 20; - - class Count { - @tracked - get id() { - return ++count; - } - } - - let obj = new Count(); - - get(obj, 'id'); - - this.assert.equal(count, 21); - } } ); @@ -90,11 +73,11 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { let obj = createObj(); for (let key in obj) { - this.assert.equal(getWithDefault(obj, key as any, 'fail'), obj[key], key); + this.assert.equal(getWithDefault(obj, key, 'fail'), obj[key], key); } class Obj { - @tracked undef: string | undefined = undefined; + @tracked undef = undefined; } let obj2 = new Obj(); @@ -105,7 +88,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { 'explicit undefined retrieves the default' ); this.assert.equal( - getWithDefault(obj2, 'not-present' as any, 'default'), + getWithDefault(obj2, 'not-present', 'default'), 'default', 'non-present key retrieves the default' ); diff --git a/packages/@ember/-internals/metal/tests/tracked/set_test.ts b/packages/@ember/-internals/metal/tests/tracked/set_test.js similarity index 91% rename from packages/@ember/-internals/metal/tests/tracked/set_test.ts rename to packages/@ember/-internals/metal/tests/tracked/set_test.js index 4458c005595..a80befbfa1d 100644 --- a/packages/@ember/-internals/metal/tests/tracked/set_test.ts +++ b/packages/@ember/-internals/metal/tests/tracked/set_test.js @@ -4,7 +4,7 @@ import { get, set, tracked } from '../..'; import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; if (EMBER_METAL_TRACKED_PROPERTIES) { - const createObj = () => { + let createObj = () => { class Obj { @tracked string = 'string'; @tracked number = 23; @@ -20,7 +20,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { moduleFor( '@tracked set', class extends AbstractTestCase { - ['@test should set arbitrary properties on an object'](assert: Assert) { + ['@test should set arbitrary properties on an object'](assert) { let obj = createObj(); class Obj { @@ -35,7 +35,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { } } - ['@test should set a number key on an object'](assert: Assert) { + ['@test should set a number key on an object'](assert) { class Obj { @tracked 1 = 'original'; } diff --git a/packages/@ember/-internals/metal/tests/tracked/support.js b/packages/@ember/-internals/metal/tests/tracked/support.js index 6d4bc761a78..2cf046adcfa 100644 --- a/packages/@ember/-internals/metal/tests/tracked/support.js +++ b/packages/@ember/-internals/metal/tests/tracked/support.js @@ -1,4 +1,4 @@ -import { tracked } from '../..'; +import { tracked, getCurrentTracker, setCurrentTracker } from '../..'; export function createTracked(values, proto = {}) { function Class() { @@ -35,3 +35,17 @@ export function createWithDescriptors(values) { return new Class(); } + +/** + Creates an autotrack stack so we can test field changes as they flow through + getters/setters, and through the system overall +*/ +export function track(fn) { + let parent = getCurrentTracker(); + let tracker = setCurrentTracker(); + + fn(); + + setCurrentTracker(parent); + return tracker.combine(); +} diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.ts b/packages/@ember/-internals/metal/tests/tracked/validation_test.js similarity index 59% rename from packages/@ember/-internals/metal/tests/tracked/validation_test.ts rename to packages/@ember/-internals/metal/tests/tracked/validation_test.js index b8380f93b75..360301fa3d4 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.ts +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -2,23 +2,50 @@ import { computed, defineProperty, get, set, tagForProperty, tracked } from '../ import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; +import { track } from './support'; if (EMBER_METAL_TRACKED_PROPERTIES) { moduleFor( '@tracked get validation', class extends AbstractTestCase { - [`@test validators for tracked getters with dependencies should invalidate when the dependencies invalidate`]( - assert: Assert - ) { + [`@test autotracking should work with tracked fields`](assert) { + class Tracked { + @tracked first = undefined; + constructor(first) { + this.first = first; + } + } + + let obj = new Tracked('Tom', 'Dale'); + + let tag = track(() => obj.first); + let snapshot = tag.value(); + + assert.equal(obj.first, 'Tom', 'The full name starts correct'); + assert.equal(tag.validate(snapshot), true); + + snapshot = tag.value(); + assert.equal(tag.validate(snapshot), true); + + obj.first = 'Thomas'; + + assert.equal(tag.validate(snapshot), false); + + assert.equal(obj.first, 'Thomas'); + snapshot = tag.value(); + + assert.equal(tag.validate(snapshot), true); + } + + [`@test autotracking should work with native getters`](assert) { class Tracked { - @tracked first?: string = undefined; - @tracked last?: string = undefined; - constructor(first: string, last: string) { + @tracked first = undefined; + @tracked last = undefined; + constructor(first, last) { this.first = first; this.last = last; } - @tracked get full() { return `${this.first} ${this.last}`; } @@ -26,11 +53,10 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { let obj = new Tracked('Tom', 'Dale'); - let tag = tagForProperty(obj, 'full'); + let tag = track(() => obj.full); let snapshot = tag.value(); - let full = obj.full; - assert.equal(full, 'Tom Dale', 'The full name starts correct'); + assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); assert.equal(tag.validate(snapshot), true); snapshot = tag.value(); @@ -46,22 +72,60 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { assert.equal(tag.validate(snapshot), true); } + [`@test autotracking should work with native setters`](assert) { + class Tracked { + @tracked first = undefined; + @tracked last = undefined; + constructor(first, last) { + this.first = first; + this.last = last; + } + + get full() { + return `${this.first} ${this.last}`; + } + + set full(value) { + let [first, last] = value.split(' '); + + this.first = first; + this.last = last; + } + } + + let obj = new Tracked('Tom', 'Dale'); + + let tag = track(() => obj.full); + let snapshot = tag.value(); + + assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); + assert.equal(tag.validate(snapshot), true); + + snapshot = tag.value(); + assert.equal(tag.validate(snapshot), true); + + obj.full = 'Melanie Sumner'; + + assert.equal(tag.validate(snapshot), false); + + assert.equal(obj.full, 'Melanie Sumner'); + assert.equal(obj.first, 'Melanie'); + assert.equal(obj.last, 'Sumner'); + snapshot = tag.value(); + + assert.equal(tag.validate(snapshot), true); + } + [`@test interaction with Ember object model (tracked property depending on Ember property)`]( - assert: Assert + assert ) { - interface NameInterface { - first: string; - last: string; - } class Tracked { - @tracked name: NameInterface; - constructor(name: NameInterface) { + constructor(name) { this.name = name; } - @tracked get full() { - return `${get(this.name, 'first')} ${get(this.name, 'last')}`; + return `${get(this, 'name.first')} ${get(this, 'name.last')}`; } } @@ -69,11 +133,10 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { let obj = new Tracked(tom); - let tag = tagForProperty(obj, 'full'); + let tag = track(() => obj.full); let snapshot = tag.value(); - let full = obj.full; - assert.equal(full, 'Tom Dale'); + assert.equal(obj.full, 'Tom Dale'); assert.equal(tag.validate(snapshot), true); snapshot = tag.value(); @@ -86,14 +149,22 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { snapshot = tag.value(); assert.equal(tag.validate(snapshot), true); + + set(obj, 'name', { first: 'Ricardo', last: 'Mendes' }); + + assert.equal(tag.validate(snapshot), false, 'invalid after setting with Ember set'); + + assert.equal(obj.full, 'Ricardo Mendes'); + snapshot = tag.value(); + + assert.equal(tag.validate(snapshot), true); } [`@test interaction with Ember object model (Ember computed property depending on tracked property)`]( - assert: Assert + assert ) { class EmberObject { - name: Name; - constructor(name: Name) { + constructor(name) { this.name = name; } } @@ -101,16 +172,17 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { defineProperty( EmberObject.prototype, 'full', - computed('name', function(this: EmberObject) { + computed('name', function() { let name = get(this, 'name'); return `${name.first} ${name.last}`; }) ); class Name { - @tracked first: string; - @tracked last: string; - constructor(first: string, last: string) { + @tracked first; + @tracked last; + + constructor(first, last) { this.first = first; this.last = last; } @@ -143,12 +215,12 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { } ['@test interaction with the Ember object model (paths going through tracked properties)']( - assert: Assert + assert ) { - let self: EmberObject; + let self; class EmberObject { - contact: Contact; - constructor(contact: Contact) { + contact; + constructor(contact) { this.contact = contact; self = this; } @@ -164,16 +236,16 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { ); class Contact { - @tracked name?: EmberName = undefined; - constructor(name: EmberName) { + @tracked name = undefined; + constructor(name) { this.name = name; } } class EmberName { - first: string; - last: string; - constructor(first: string, last: string) { + first; + last; + constructor(first, last) { this.first = first; this.last = last; }