Skip to content

Commit

Permalink
[FEAT] Updates @Tracked to match the Tracked Properties RFC
Browse files Browse the repository at this point in the history
Updates @Tracked to be a classic decorator as well as a standard
native decorator, removes the ability to decorator native getters, and
adds more tests for various situations and updates the existing tests.

One thing to note is that users can still provide getters/setters to
classic classes via tracked:

```js
import EmberObject from '@ember/object';
import { tracked } from '@glimmer/tracking';

const Person = EmberObject.extend({
  firstName: tracked({ value: 'Tom' }),
  lastName: tracked({ value: 'Dale' }),

  fullName: tracked({
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
  }),
});
```

The reasoning behind this is there is no way to do this currently, and
it allows us to match behavior between native/classic exactly.
Alternatively we could expose the native descriptor decorator that is
internal only right now, but unfortunately `computed` will not be able
to fill this role since it _requires_ use of `Ember.set`, where native
getters/setters do not.
  • Loading branch information
Chris Garrett committed Feb 6, 2019
1 parent df39f05 commit 93b8039
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 243 deletions.
2 changes: 1 addition & 1 deletion packages/@ember/-internals/metal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 14 additions & 5 deletions packages/@ember/-internals/metal/lib/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { unwatch, watch } from './watching';

const DECORATOR_DESCRIPTOR_MAP: WeakMap<Decorator, any> = 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<Decorator, any>;

// https://tc39.github.io/proposal-decorators/#sec-elementdescriptor-specification-type
export interface ElementDescriptor {
Expand Down Expand Up @@ -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);
}

// ..........................................................
Expand Down Expand Up @@ -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';
Expand All @@ -232,7 +241,7 @@ export function makeComputedDecorator(
return elementDesc;
};

DECORATOR_DESCRIPTOR_MAP.set(decorator, desc);
setComputedDecorator(decorator, desc);

Object.setPrototypeOf(decorator, DecoratorClass.prototype);

Expand Down
9 changes: 6 additions & 3 deletions packages/@ember/-internals/metal/lib/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
202 changes: 100 additions & 102 deletions packages/@ember/-internals/metal/lib/tracked.ts
Original file line number Diff line number Diff line change
@@ -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> = T | null;
type unusable = null | undefined | void | {};

/**
An object that that tracks @tracked properties that were consumed.
@private
*/
*/
class Tracker {
private tags = new Set<Tag>();
private last: Option<Tag> = null;
Expand Down Expand Up @@ -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
Expand All @@ -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<Tracker> = null;

export function getCurrentTracker(): Option<Tracker> {
Expand All @@ -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;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/@ember/-internals/metal/tests/accessors/set_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
);
Loading

0 comments on commit 93b8039

Please sign in to comment.