Skip to content

Commit 2ba9d95

Browse files
author
Chris Garrett
committed
[BUGFIX] Fix reactivity of dynamic attributes
The updates to use autotracking through the VM resulted in a bug in dynamic attributes. Previously, the updating opcode for dynamic attributes would run its update function based on tags/autotracking - whenever any tag updated (e.g. a tracked value was changed) the attribute would call its `update` function. The `update` function would then check the new value against the current value of the attribute, and update if they were different. The bug was we introduced a value comparison in the updating opcode itself, based on the last value of the property to set the attribute to. This meant that if the property was updated to the same value twice in a row, it would not call `update`, even if the attribute's value had changed (e.g. because the user typed something).
1 parent ebd95ed commit 2ba9d95

File tree

2 files changed

+57
-15
lines changed

2 files changed

+57
-15
lines changed

packages/@glimmer/integration-tests/test/attributes-test.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { normalizeProperty } from '@glimmer/runtime';
2-
import { assertElement, hasAttribute, jitSuite, RenderTest, test } from '..';
2+
import { assertElement, hasAttribute, jitSuite, RenderTest, test, tracked } from '..';
33
import { Namespace, SimpleElement } from '@simple-dom/interface';
4+
import { castToBrowser, expect } from '@glimmer/util';
45

56
export class AttributesTests extends RenderTest {
67
static suiteName = 'Attributes';
@@ -239,6 +240,40 @@ export class AttributesTests extends RenderTest {
239240
this.assertStableNodes();
240241
}
241242

243+
@test
244+
'handles successive updates to the same value'() {
245+
class Model {
246+
@tracked value = '';
247+
}
248+
249+
let model = new Model();
250+
251+
this.render('<input value={{this.model.value}} />', { model });
252+
this.assert.equal(this.readDOMAttr('value'), '');
253+
this.assertStableRerender();
254+
255+
let inputElement = castToBrowser(
256+
expect(this.element.firstChild, 'expected input to exist'),
257+
'input'
258+
);
259+
260+
inputElement.value = 'bar';
261+
this.assert.equal(this.readDOMAttr('value'), 'bar');
262+
263+
model.value = 'foo';
264+
this.rerender();
265+
this.assert.equal(this.readDOMAttr('value'), 'foo');
266+
this.assertStableNodes();
267+
268+
inputElement.value = 'bar';
269+
this.assert.equal(this.readDOMAttr('value'), 'bar');
270+
271+
model.value = 'foo';
272+
this.rerender();
273+
this.assert.equal(this.readDOMAttr('value'), 'foo');
274+
this.assertStableNodes();
275+
}
276+
242277
@test
243278
'input[checked] prop updates when set to undefined'() {
244279
this.registerHelper('if', (params) => {

packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Reference, valueForRef, isConstRef } from '@glimmer/reference';
1+
import { Reference, valueForRef, isConstRef, createComputeRef } from '@glimmer/reference';
22
import { Revision, Tag, valueForTag, validateTag, consumeTag } from '@glimmer/validator';
33
import {
44
check,
@@ -16,7 +16,7 @@ import {
1616
VMArguments,
1717
Owner,
1818
CurriedType,
19-
ModifierDefinitionState,
19+
ModifierDefinitionState,, Environment
2020
} from '@glimmer/interfaces';
2121
import { $t0 } from '@glimmer/vm';
2222
import { APPEND_OPCODES, UpdatingOpcode } from '../../opcodes';
@@ -237,27 +237,34 @@ APPEND_OPCODES.add(Op.DynamicAttr, (vm, { op1: _name, op2: _trusting, op3: _name
237237
let attribute = vm.elements().setDynamicAttribute(name, value, trusting, namespace);
238238

239239
if (!isConstRef(reference)) {
240-
vm.updateWith(new UpdateDynamicAttributeOpcode(reference, attribute));
240+
vm.updateWith(new UpdateDynamicAttributeOpcode(reference, attribute, vm.env));
241241
}
242242
});
243243

244244
export class UpdateDynamicAttributeOpcode extends UpdatingOpcode {
245245
public type = 'patch-element';
246246

247-
public lastValue: unknown;
247+
private updateRef: Reference;
248248

249-
constructor(private reference: Reference<unknown>, private attribute: DynamicAttribute) {
249+
constructor(reference: Reference<unknown>, attribute: DynamicAttribute, env: Environment) {
250250
super();
251-
this.lastValue = valueForRef(reference);
252-
}
253251

254-
evaluate(vm: UpdatingVM) {
255-
let { attribute, reference, lastValue } = this;
256-
let currentValue = valueForRef(reference);
252+
let initialized = false;
257253

258-
if (currentValue !== lastValue) {
259-
attribute.update(currentValue, vm.env);
260-
this.lastValue = currentValue;
261-
}
254+
this.updateRef = createComputeRef(() => {
255+
let value = valueForRef(reference);
256+
257+
if (initialized === true) {
258+
attribute.update(value, env);
259+
} else {
260+
initialized = true;
261+
}
262+
});
263+
264+
valueForRef(this.updateRef);
265+
}
266+
267+
evaluate() {
268+
valueForRef(this.updateRef);
262269
}
263270
}

0 commit comments

Comments
 (0)