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.

This PR also exposes the native `descriptor` decorator for use defining
getters and setters:

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

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

  fullName: descriptor({
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
  }),
});
```
  • Loading branch information
Chris Garrett committed Feb 8, 2019
1 parent c91bcd2 commit 99cb377
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 283 deletions.
15 changes: 14 additions & 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 All @@ -64,3 +64,16 @@ export {
isSearchDisabled as isNamespaceSearchDisabled,
setSearchDisabled as setNamespaceSearchDisabled,
} from './lib/namespace_search';

import { DEBUG } from '@glimmer/env';
import { setComputedDecorator } from './lib/decorator';
import { tracked } from './lib/tracked';

// We have to set this here because there is a cycle of dependencies in tracked
// which causes `setComputedDecorator` to not be resolved before the `tracked`
// module.
if (DEBUG) {
// Normally this isn't a classic decorator, but we want to throw a helpful
// error in development so we need it to treat it like one
setComputedDecorator(tracked);
}
8 changes: 4 additions & 4 deletions packages/@ember/-internals/metal/lib/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ 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) {
DECORATOR_DESCRIPTOR_MAP.set(dec, value);
}

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

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

Object.setPrototypeOf(decorator, DecoratorClass.prototype);

Expand Down
10 changes: 6 additions & 4 deletions packages/@ember/-internals/metal/lib/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/

import { Meta, meta as metaFor, peekMeta, UNDEFINED } from '@ember/-internals/meta';
import { EMBER_NATIVE_DECORATOR_SUPPORT } from '@ember/canary-features';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import {
Expand Down Expand Up @@ -155,16 +154,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
206 changes: 106 additions & 100 deletions packages/@ember/-internals/metal/lib/tracked.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { EMBER_NATIVE_DECORATOR_SUPPORT } from '@ember/canary-features';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { combine, CONSTANT_TAG, Tag } from '@glimmer/reference';
import { dirty, tagFor, tagForProperty, update } from './tags';
import { Decorator, ElementDescriptor, setComputedDecorator } from './decorator';
import { dirty, tagFor, tagForProperty } from './tags';

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,134 +98,137 @@ 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(propertyDesc: { value: any }): Decorator;
export function tracked(elementDesc: ElementDescriptor): ElementDescriptor;
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);
}
}
elementDesc: ElementDescriptor | any,
isClassicDecorator?: boolean
): ElementDescriptor | Decorator {
if (
elementDesc === undefined ||
elementDesc === null ||
elementDesc.toString() !== '[object Descriptor]'
) {
assert(
`tracked() may only receive an options object containing 'value' or 'initializer', received ${elementDesc}`,
elementDesc === undefined || (elementDesc !== null && typeof elementDesc === 'object')
);

/**
@private
if (DEBUG && elementDesc) {
let keys = Object.keys(elementDesc);

Whenever a tracked computed property is entered, the current tracker is
saved off and a new tracker is replaced.
assert(
`The options object passed to tracked() may only contain a 'value' or 'initializer' property, not both. Received: [${keys}]`,
keys.length <= 1 &&
(keys[0] === undefined || keys[0] === 'value' || keys[0] === 'undefined')
);

Any tracked properties consumed are added to the current tracker.
assert(
`The initializer passed to tracked must be a function. Received ${elementDesc.initializer}`,
!('initializer' in elementDesc) || typeof elementDesc.initializer === 'function'
);
}

When a tracked computed property is exited, the tracker's tags are
combined and added to the parent tracker.
let initializer = elementDesc ? elementDesc.initializer : undefined;
let value = elementDesc ? elementDesc.value : undefined;

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;
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
);

export function getCurrentTracker(): Option<Tracker> {
return CURRENT_TRACKER;
}
elementDesc.initializer = initializer || (() => value);

export function setCurrentTracker(tracker: Tracker = new Tracker()): Tracker {
return (CURRENT_TRACKER = tracker);
}
return descriptorForField(elementDesc);
};

function descriptorForAccessor(
key: string | symbol,
descriptor: PropertyDescriptor
): PropertyDescriptor {
let get = descriptor.get as Function;
let set = descriptor.set as Function;
setComputedDecorator(decorator);

function getter(this: any): any {
// Swap the parent tracker for a new tracker
let old = CURRENT_TRACKER;
let tracker = (CURRENT_TRACKER = new Tracker());
return decorator;
}

// Call the getter
let ret = get.call(this);
assert(
'Native decorators are not enabled without the EMBER_NATIVE_DECORATOR_SUPPORT flag',
Boolean(EMBER_NATIVE_DECORATOR_SUPPORT)
);

// Swap back the parent tracker
CURRENT_TRACKER = old;
assert(
`@tracked can only be used directly as a native decorator. If you're using tracked in classic classes, add parenthesis to call it like a function: tracked()`,
!isClassicDecorator
);

// 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);
return descriptorForField(elementDesc);
}

// Update the UpdatableTag for this property with the tag for all of the
// consumed dependencies.
update(tagForProperty(this, key), tag);
function descriptorForField(elementDesc: ElementDescriptor): ElementDescriptor {
let { key, kind, initializer } = elementDesc as ElementDescriptor;

return ret;
}
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'
);

function setter(this: unusable): void {
dirty(tagForProperty(this, key));
set.apply(this, arguments);
}
let shadowKey = Symbol(key);

return {
enumerable: true,
configurable: false,
get: get && getter,
set: set && setter,
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();
},
},
};
}

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);
Whenever a tracked computed property is entered, the current tracker is
saved off and a new tracker is replaced.
return {
enumerable: true,
configurable: true,
Any tracked properties consumed are added to the current tracker.
get(): any {
if (CURRENT_TRACKER) CURRENT_TRACKER.add(tagForProperty(this, key));
When a tracked computed property is exited, the tracker's tags are
combined and added to the parent tracker.
if (!(shadowKey in this)) {
this[shadowKey] = descriptor.value;
}
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;

return this[shadowKey];
},
export function getCurrentTracker(): Option<Tracker> {
return CURRENT_TRACKER;
}

set(newValue: any): void {
tagFor(this).inner!['dirty']();
dirty(tagForProperty(this, key));
this[shadowKey] = newValue;
propertyDidChange();
},
};
export function setCurrentTracker(tracker: Tracker = new Tracker()): Tracker {
return (CURRENT_TRACKER = tracker);
}

export type Key = string;

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 99cb377

Please sign in to comment.