Skip to content

Commit 536df75

Browse files
committed
update indirectly-affected bindings on mutation
1 parent d941cf5 commit 536df75

File tree

4 files changed

+56
-62
lines changed

4 files changed

+56
-62
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { regex_starts_with_newline } from '../../patterns.js';
1212
import { check_element } from './shared/a11y.js';
1313
import { validate_element } from './shared/element.js';
1414
import { mark_subtree_dynamic } from './shared/fragment.js';
15+
import { object } from '../../../utils/ast.js';
1516

1617
/**
1718
* @param {AST.RegularElement} node
@@ -59,6 +60,34 @@ export function RegularElement(node, context) {
5960
}
6061
}
6162

63+
// Special case: `<select bind:value={foo}><option>{bar}</option>`
64+
// means we need to invalidate `bar` whenever `foo` is mutated
65+
if (node.name === 'select') {
66+
for (const attribute of node.attributes) {
67+
if (
68+
attribute.type === 'BindDirective' &&
69+
attribute.name === 'value' &&
70+
attribute.expression.type !== 'SequenceExpression'
71+
) {
72+
const identifier = object(attribute.expression);
73+
const binding = identifier && context.state.scope.get(identifier.name);
74+
75+
if (binding) {
76+
for (const name of context.state.scope.references.keys()) {
77+
if (name === binding.node.name) continue;
78+
const indirect = context.state.scope.get(name);
79+
80+
if (indirect) {
81+
binding.legacy_indirect_bindings.add(indirect);
82+
}
83+
}
84+
}
85+
86+
break;
87+
}
88+
}
89+
}
90+
6291
// Special case: single expression tag child of option element -> add "fake" attribute
6392
// to ensure that value types are the same (else for example numbers would be strings)
6493
if (

packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
is_event_attribute
99
} from '../../../../utils/ast.js';
1010
import { dev, locate_node } from '../../../../state.js';
11-
import { should_proxy } from '../utils.js';
11+
import { build_getter, should_proxy } from '../utils.js';
1212
import { visit_assignment_expression } from '../../shared/assignments.js';
1313
import { validate_mutation } from './shared/utils.js';
1414
import { get_rune } from '../../../scope.js';
@@ -146,14 +146,33 @@ function build_assignment(operator, left, right, context) {
146146

147147
// mutation
148148
if (transform?.mutate) {
149-
return transform.mutate(
149+
let mutation = transform.mutate(
150150
object,
151151
b.assignment(
152152
operator,
153153
/** @type {Pattern} */ (context.visit(left)),
154154
/** @type {Expression} */ (context.visit(right))
155155
)
156156
);
157+
158+
if (binding.legacy_indirect_bindings.size > 0) {
159+
mutation = b.sequence([
160+
mutation,
161+
b.call(
162+
'$.invalidate_inner_signals',
163+
b.arrow(
164+
[],
165+
b.block(
166+
Array.from(binding.legacy_indirect_bindings).map((binding) =>
167+
b.stmt(build_getter({ ...binding.node }, context.state))
168+
)
169+
)
170+
)
171+
)
172+
]);
173+
}
174+
175+
return mutation;
157176
}
158177

159178
// in cases like `(object.items ??= []).push(value)`, we may need to warn

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,6 @@ export function RegularElement(node, context) {
191191
}
192192
}
193193

194-
if (node.name === 'select' && bindings.has('value')) {
195-
setup_select_synchronization(/** @type {AST.BindDirective} */ (bindings.get('value')), context);
196-
}
197-
198194
// Let bindings first, they can be used on attributes
199195
context.state.init.push(...lets);
200196

@@ -395,62 +391,6 @@ export function RegularElement(node, context) {
395391
context.state.template.pop_element();
396392
}
397393

398-
/**
399-
* Special case: if we have a value binding on a select element, we need to set up synchronization
400-
* between the value binding and inner signals, for indirect updates
401-
* @param {AST.BindDirective} value_binding
402-
* @param {ComponentContext} context
403-
*/
404-
function setup_select_synchronization(value_binding, context) {
405-
if (context.state.analysis.runes) return;
406-
407-
let bound = value_binding.expression;
408-
409-
if (bound.type === 'SequenceExpression') {
410-
return;
411-
}
412-
413-
while (bound.type === 'MemberExpression') {
414-
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
415-
}
416-
417-
/** @type {string[]} */
418-
const names = [];
419-
420-
for (const [name, refs] of context.state.scope.references) {
421-
if (
422-
refs.length > 0 &&
423-
// prevent infinite loop
424-
name !== bound.name
425-
) {
426-
names.push(name);
427-
}
428-
}
429-
430-
const invalidator = b.call(
431-
'$.invalidate_inner_signals',
432-
b.thunk(
433-
b.block(
434-
names.map((name) => {
435-
const serialized = build_getter(b.id(name), context.state);
436-
return b.stmt(serialized);
437-
})
438-
)
439-
)
440-
);
441-
442-
context.state.init.push(
443-
b.stmt(
444-
b.call(
445-
'$.template_effect',
446-
b.thunk(
447-
b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)])
448-
)
449-
)
450-
)
451-
);
452-
}
453-
454394
/**
455395
* @param {AST.ClassDirective[]} class_directives
456396
* @param {Expression[]} expressions

packages/svelte/src/compiler/phases/scope.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ export class Binding {
114114
*/
115115
legacy_dependencies = [];
116116

117+
/**
118+
* Bindings that should be invalidated when this binding is invalidated
119+
* @type {Set<Binding>}
120+
*/
121+
legacy_indirect_bindings = new Set();
122+
117123
/**
118124
* Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
119125
* @type {string | null}

0 commit comments

Comments
 (0)