Skip to content

Commit 58c2182

Browse files
Merge pull request #21471 from emberjs/nvp/rfc-1071-tracked-overload
Implement RFC #1071: overloaded tracked
2 parents 2b78613 + 22ece72 commit 58c2182

13 files changed

Lines changed: 1029 additions & 86 deletions

File tree

‎packages/@ember/-internals/glimmer/lib/glimmer-tracking-docs.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,74 @@
149149
});
150150
```
151151
152+
Calling `tracked` with an initial value creates a standalone reactive
153+
value, usable outside of classes:
154+
155+
```js
156+
import { tracked } from '@glimmer/tracking';
157+
158+
const count = tracked(0);
159+
160+
count.value; // read the value, entangling with any tracking context
161+
count.value = 1; // write the value, notifying consumers
162+
count.get(); // function shorthand for reading
163+
count.set(2); // function shorthand for writing
164+
count.update((n) => n + 1); // write based on the current value, without entangling
165+
count.freeze(); // prevent all future writes
166+
```
167+
168+
Reading `value` in a template (or in a getter used by a template) will
169+
rerender just like a `@tracked` property:
170+
171+
```gjs
172+
import { tracked } from '@glimmer/tracking';
173+
import { on } from '@ember/modifier';
174+
175+
const count = tracked(0);
176+
const increment = () => count.value++;
177+
178+
<template>
179+
Count is: {{count.value}}
180+
181+
<button {{on "click" increment}}>add one</button>
182+
</template>
183+
```
184+
185+
This form accepts an options object containing an `equals` function, which
186+
decides whether a written value should notify consumers (it defaults to
187+
`Object.is`), and a `description` used for debugging:
188+
189+
```js
190+
const count = tracked(0, { equals: (a, b) => a === b });
191+
192+
count.value = 0; // does not notify consumers, the value did not change
193+
```
194+
195+
Note: when `tracked` is called with a single plain object whose keys are all
196+
drawn from `value`, `initializer`, `equals`, and `description`, it is treated
197+
as the configuration object for a decorator (or classic-class field), not as a
198+
standalone value wrapping that object. To create a standalone reactive value
199+
around such an object, pass it as an initial value and include a (possibly
200+
empty) options argument: `tracked({ value: 5 }, {})`.
201+
202+
The `@tracked` decorator accepts the same options. By default, setting a
203+
`@tracked` property always notifies consumers, even when setting the
204+
property to the same value; passing `equals` opts in to equality-based
205+
notification instead:
206+
207+
```js
208+
import { tracked } from '@glimmer/tracking';
209+
210+
class Counter {
211+
@tracked({ equals: (a, b) => a === b }) count = 0;
212+
213+
noop = () => {
214+
// does not notify consumers, the value did not change
215+
this.count = this.count;
216+
};
217+
}
218+
```
219+
152220
@method tracked
153221
@static
154222
@for @glimmer/tracking

‎packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
55
import { tracked } from '@ember/-internals/metal';
66
import { computed, get, set } from '@ember/object';
77
import { Promise } from 'rsvp';
8+
import { fn } from '@ember/helper';
9+
import { on } from '@ember/modifier';
810
import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers';
911
import GlimmerishComponent from '../../utils/glimmerish-component';
1012
import { Component } from '../../utils/helpers';
@@ -204,6 +206,112 @@ moduleFor(
204206
this.assertText('1');
205207
}
206208

209+
'@test standalone tracked values rerender when updated'() {
210+
class CountComponent extends Component {
211+
count = tracked(0);
212+
213+
increment = () => {
214+
this.count.value++;
215+
};
216+
}
217+
218+
this.owner.register(
219+
'component:counter',
220+
setComponentTemplate(
221+
precompileTemplate('<button {{on "click" this.increment}}>{{this.count.value}}</button>'),
222+
CountComponent
223+
)
224+
);
225+
226+
this.render('<Counter />');
227+
228+
this.assertText('0');
229+
230+
runTask(() => this.$('button').click());
231+
232+
this.assertText('1');
233+
}
234+
235+
'@test standalone tracked values in module scope rerender when updated'() {
236+
let count = tracked(0);
237+
238+
class CountComponent extends Component {
239+
count = count;
240+
}
241+
242+
this.owner.register(
243+
'component:counter',
244+
setComponentTemplate(precompileTemplate('{{this.count.value}}'), CountComponent)
245+
);
246+
247+
this.render('<Counter />');
248+
249+
this.assertText('0');
250+
251+
runTask(() => count.set(1));
252+
253+
this.assertText('1');
254+
255+
runTask(() => count.update((value) => value + 1));
256+
257+
this.assertText('2');
258+
}
259+
260+
'@test standalone tracked values do not rerender when set to an equal value'(assert) {
261+
let count = tracked(0);
262+
let evaluations = 0;
263+
264+
class CountComponent extends Component {
265+
get count() {
266+
evaluations++;
267+
return count.value;
268+
}
269+
}
270+
271+
this.owner.register(
272+
'component:counter',
273+
setComponentTemplate(precompileTemplate('{{this.count}}'), CountComponent)
274+
);
275+
276+
this.render('<Counter />');
277+
278+
this.assertText('0');
279+
assert.strictEqual(evaluations, 1, 'rendered once');
280+
281+
runTask(() => count.set(0));
282+
283+
this.assertText('0');
284+
assert.strictEqual(evaluations, 1, 'setting an equal value does not rerender');
285+
286+
runTask(() => count.set(1));
287+
288+
this.assertText('1');
289+
assert.strictEqual(evaluations, 2, 'setting a new value rerenders');
290+
}
291+
292+
'@test tracked can be used as a helper in templates'() {
293+
let increment = (count) => count.value++;
294+
295+
this.owner.register(
296+
'component:counter',
297+
setComponentTemplate(
298+
precompileTemplate(
299+
'{{#let (tracked 0) as |count|}}<button {{on "click" (fn increment count)}}>{{count.value}}</button>{{/let}}',
300+
{ strictMode: true, scope: () => ({ tracked, on, fn, increment }) }
301+
),
302+
class extends GlimmerishComponent {}
303+
)
304+
);
305+
306+
this.render('<Counter />');
307+
308+
this.assertText('0');
309+
310+
runTask(() => this.$('button').click());
311+
312+
this.assertText('1');
313+
}
314+
207315
'@test tracked properties rerender when updated outside of a runloop'(assert) {
208316
let done = assert.async();
209317

‎packages/@ember/-internals/metal/lib/properties.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,10 @@ export function defineDecorator(
9595
desc: ExtendedMethodDecorator,
9696
meta: Meta
9797
) {
98-
let propertyDesc;
99-
100-
if (DEBUG) {
101-
propertyDesc = desc(obj, keyName, undefined, meta, true);
102-
} else {
103-
propertyDesc = desc(obj, keyName, undefined, meta);
104-
}
98+
let propertyDesc = desc(obj, keyName, undefined, meta, true);
10599

106100
Object.defineProperty(obj, keyName, propertyDesc as PropertyDescriptor);
107101

108-
// pass the decorator function forward for backwards compat
109102
return desc;
110103
}
111104

0 commit comments

Comments
 (0)