Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUGFIX lts] Ensure {{each-in}} can iterate over keys with periods #18296

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions packages/@ember/-internals/glimmer/lib/utils/iterable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { get, objectAt, tagFor, tagForProperty } from '@ember/-internals/metal';
import {
consume,
get,
isTracking,
objectAt,
tagFor,
tagForProperty,
} from '@ember/-internals/metal';
import { _contentFor } from '@ember/-internals/runtime';
import { guidFor, HAS_NATIVE_SYMBOL, isEmberArray, isProxy } from '@ember/-internals/utils';
import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
import { assert } from '@ember/debug';
import {
AbstractIterable,
Expand Down Expand Up @@ -127,7 +135,22 @@ class ObjectIterator extends BoundedIterator {
} else {
let values: Opaque[] = [];
for (let i = 0; i < length; i++) {
values.push(get(obj, keys[i]));
let value: any;
let key = keys[i];

value = obj[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks when obj is a proxy, is that okay?

In general, if the solution requires avoiding get semantics, I think that is not okay. However, maybe in this case it is fine since we already did Object.keys above so it won't trigger unknownProperty anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, I started writing the code to do unknownProperty and realized that by definition, it could never run, which is why I omitted it.


// Add the tag of the returned value if it is an array, since arrays
// should always cause updates if they are consumed and then changed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that right? If you pass this to, e.g. #each, we handle this there already. If you just pass the value to, say, a component or a helper, I am not sure we need to invalidate those if the content of the array changes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test you added should pass without consuming [].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of autotracking as a whole, it allows consumers to use arrays (both native and non-native) anywhere they consume a tracked value, and have mutations to that array be autotracked. In practice, this is like the native getters migration, it’ll mean users can access any array as if it were a normal, native array. They only need to use methods for updating the state of the array (we need native Proxy to allow them to update with native syntax).

Even when we do get to native Proxy though, this will allow us to omit a get handler in the Proxy, which should help us avoid perf issues with proxies

if (EMBER_METAL_TRACKED_PROPERTIES && isTracking()) {
consume(tagForProperty(obj, key));

if (Array.isArray(value) || isEmberArray(value)) {
consume(tagForProperty(value, '[]'));
}
}

values.push(value);
}
return new this(keys, values, length, keyFor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,48 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {

assert.strictEqual(computeCount, 2, 'compute is called exactly 2 times');
}

'@test each-in autotracks non-tracked values correctly'() {
let obj = EmberObject.create({ value: 'bob' });

this.registerComponent('person', {
ComponentClass: Component.extend({ obj }),
template: strip`
{{#each-in this.obj as |key value|}}
{{value}}-{{key}}
{{/each-in}}
`,
});

this.render('<Person/>');

this.assertText('bob-value');

runTask(() => obj.set('value', 'sal'));

this.assertText('sal-value');
}

'@test each-in autotracks arrays acorrectly'() {
let obj = EmberObject.create({ arr: A([1]) });

this.registerComponent('person', {
ComponentClass: Component.extend({ obj }),
template: strip`
{{#each-in this.obj as |key arr|}}
{{#each arr as |v|}}{{v}}{{/each}}
{{/each-in}}
`,
});

this.render('<Person/>');

this.assertText('1');

runTask(() => obj.arr.pushObject(2));

this.assertText('12');
}
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,22 @@ class EachInTest extends AbstractEachInTest {

this.assertText('Empty!');
}

[`@test it can render items that contain keys with periods in them`]() {
this.makeHash({ 'period.key': 'a', 'other.period.key': 'b' });

this.render(
`<ul>{{#each-in hash as |key value|}}<li>{{key}}: {{value}}</li>{{else}}Empty!{{/each-in}}</ul>`
);

this.assertText('period.key: aother.period.key: b');

this.assertStableRerender();

this.clear();

this.assertText('Empty!');
}
}

moduleFor(
Expand Down
2 changes: 1 addition & 1 deletion packages/@ember/-internals/metal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ 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 { default as runInTransaction, didRender, assertNotRendered } from './lib/transaction';
export { consume, Tracker, tracked, track, untrack } from './lib/tracked';
export { consume, Tracker, tracked, track, untrack, isTracking } from './lib/tracked';

export {
NAMESPACES,
Expand Down