Skip to content

Commit 484885d

Browse files
committed
component props
1 parent d700bf1 commit 484885d

File tree

5 files changed

+164
-64
lines changed

5 files changed

+164
-64
lines changed

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

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
1+
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement, SpreadElement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../../types.js' */
44
import { dev, is_ignored } from '../../../../../state.js';
@@ -8,6 +8,7 @@ import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '..
88
import { build_attribute_value } from '../shared/element.js';
99
import { build_event_handler } from './events.js';
1010
import { determine_slot } from '../../../../../utils/slot.js';
11+
import { init_spread_bindings } from '../../../shared/spread_bindings.js';
1112

1213
/**
1314
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -48,7 +49,7 @@ export function build_component(node, component_name, context) {
4849
/** @type {Property[]} */
4950
const custom_css_props = [];
5051

51-
/** @type {Identifier | MemberExpression | SequenceExpression | null} */
52+
/** @type {Identifier | MemberExpression | SequenceExpression | SpreadElement | null} */
5253
let bind_this = null;
5354

5455
/** @type {ExpressionStatement[]} */
@@ -196,84 +197,100 @@ export function build_component(node, component_name, context) {
196197
push_prop(b.init(attribute.name, value));
197198
}
198199
} else if (attribute.type === 'BindDirective') {
199-
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
200+
if (attribute.expression.type === 'SpreadElement') {
201+
const { get, set } = init_spread_bindings(attribute.expression, context);
200202

201-
if (
202-
dev &&
203-
attribute.name !== 'this' &&
204-
!is_ignored(node, 'ownership_invalid_binding') &&
205-
// bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation
206-
attribute.expression.type !== 'SequenceExpression'
207-
) {
208-
const left = object(attribute.expression);
209-
const binding = left && context.state.scope.get(left.name);
210-
211-
if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') {
212-
context.state.analysis.needs_mutation_validation = true;
213-
binding_initializers.push(
214-
b.stmt(
215-
b.call(
216-
'$$ownership_validator.binding',
217-
b.literal(binding.node.name),
218-
b.id(is_component_dynamic ? intermediate_name : component_name),
219-
b.thunk(expression)
220-
)
221-
)
222-
);
223-
}
224-
}
225-
226-
if (expression.type === 'SequenceExpression') {
227203
if (attribute.name === 'this') {
228204
bind_this = attribute.expression;
229205
} else {
230-
const [get, set] = expression.expressions;
231-
const get_id = b.id(context.state.scope.generate('bind_get'));
232-
const set_id = b.id(context.state.scope.generate('bind_set'));
233-
234-
context.state.init.push(b.var(get_id, get));
235-
context.state.init.push(b.var(set_id, set));
236-
237-
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
238-
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
206+
push_prop(b.get(attribute.name, [b.return(b.call(get))]), true);
207+
push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true);
239208
}
240209
} else {
210+
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
211+
241212
if (
242213
dev &&
243-
expression.type === 'MemberExpression' &&
244-
context.state.analysis.runes &&
245-
!is_ignored(node, 'binding_property_non_reactive')
214+
attribute.name !== 'this' &&
215+
!is_ignored(node, 'ownership_invalid_binding') &&
216+
// bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation
217+
attribute.expression.type !== 'SequenceExpression'
246218
) {
247-
validate_binding(context.state, attribute, expression);
219+
const left = object(attribute.expression);
220+
const binding = left && context.state.scope.get(left.name);
221+
222+
if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') {
223+
context.state.analysis.needs_mutation_validation = true;
224+
binding_initializers.push(
225+
b.stmt(
226+
b.call(
227+
'$$ownership_validator.binding',
228+
b.literal(binding.node.name),
229+
b.id(is_component_dynamic ? intermediate_name : component_name),
230+
b.thunk(expression)
231+
)
232+
)
233+
);
234+
}
248235
}
249236

250-
if (attribute.name === 'this') {
251-
bind_this = attribute.expression;
237+
if (expression.type === 'SequenceExpression') {
238+
if (attribute.name === 'this') {
239+
bind_this = attribute.expression;
240+
} else {
241+
const [get, set] = expression.expressions;
242+
const get_id = b.id(context.state.scope.generate('bind_get'));
243+
const set_id = b.id(context.state.scope.generate('bind_set'));
244+
245+
context.state.init.push(b.var(get_id, get));
246+
context.state.init.push(b.var(set_id, set));
247+
248+
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
249+
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
250+
}
252251
} else {
253-
const is_store_sub =
254-
attribute.expression.type === 'Identifier' &&
255-
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
252+
if (
253+
dev &&
254+
expression.type === 'MemberExpression' &&
255+
context.state.analysis.runes &&
256+
!is_ignored(node, 'binding_property_non_reactive')
257+
) {
258+
validate_binding(context.state, attribute, expression);
259+
}
260+
261+
if (attribute.name === 'this') {
262+
bind_this = attribute.expression;
263+
} else {
264+
const is_store_sub =
265+
attribute.expression.type === 'Identifier' &&
266+
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
267+
268+
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
269+
if (is_store_sub) {
270+
push_prop(
271+
b.get(attribute.name, [
272+
b.stmt(b.call('$.mark_store_binding')),
273+
b.return(expression)
274+
]),
275+
true
276+
);
277+
} else {
278+
push_prop(b.get(attribute.name, [b.return(expression)]), true);
279+
}
280+
281+
const assignment = b.assignment(
282+
'=',
283+
/** @type {Pattern} */ (attribute.expression),
284+
b.id('$$value')
285+
);
256286

257-
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
258-
if (is_store_sub) {
259287
push_prop(
260-
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
288+
b.set(attribute.name, [
289+
b.stmt(/** @type {Expression} */ (context.visit(assignment)))
290+
]),
261291
true
262292
);
263-
} else {
264-
push_prop(b.get(attribute.name, [b.return(expression)]), true);
265293
}
266-
267-
const assignment = b.assignment(
268-
'=',
269-
/** @type {Pattern} */ (attribute.expression),
270-
b.id('$$value')
271-
);
272-
273-
push_prop(
274-
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
275-
true
276-
);
277294
}
278295
}
279296
} else if (attribute.type === 'AttachTag') {

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { empty_comment, build_attribute_value } from './utils.js';
55
import * as b from '#compiler/builders';
66
import { is_element_node } from '../../../../nodes.js';
77
import { dev } from '../../../../../state.js';
8+
import { init_spread_bindings } from '../../../shared/spread_bindings.js';
89

910
/**
1011
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -93,7 +94,17 @@ export function build_inline_component(node, expression, context) {
9394
const value = build_attribute_value(attribute.value, context, false, true);
9495
push_prop(b.prop('init', b.key(attribute.name), value));
9596
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
96-
if (attribute.expression.type === 'SequenceExpression') {
97+
if (attribute.expression.type === 'SpreadElement') {
98+
const { get, set } = init_spread_bindings(attribute.expression, context);
99+
100+
push_prop(b.get(attribute.name, [b.return(b.call(get))]));
101+
push_prop(
102+
b.set(attribute.name, [
103+
b.stmt(b.call(set, b.id('$$value'))),
104+
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
105+
])
106+
);
107+
} else if (attribute.expression.type === 'SequenceExpression') {
97108
const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
98109
.expressions;
99110
const get_id = b.id(context.state.scope.generate('bind_get'));
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
let { a = $bindable() } = $props();
3+
4+
const bindings = $derived([
5+
() => a,
6+
(v) => {
7+
console.log('b', v);
8+
a = v;
9+
}
10+
]);
11+
</script>
12+
13+
<input
14+
type="value"
15+
bind:value={...bindings}
16+
/>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
import { assert_ok } from '../../../suite';
4+
5+
export default test({
6+
async test({ assert, target, logs }) {
7+
const [input, checkbox] = target.querySelectorAll('input');
8+
9+
input.value = '2';
10+
input.dispatchEvent(new window.Event('input'));
11+
12+
flushSync();
13+
14+
assert.htmlEqual(
15+
target.innerHTML,
16+
`<button>a: 2</button><input type="value"><div><input type="checkbox" ></div>`
17+
);
18+
19+
assert.deepEqual(logs, ['b', '2', 'a', '2']);
20+
21+
flushSync(() => {
22+
checkbox.click();
23+
});
24+
assert.deepEqual(logs, ['b', '2', 'a', '2', 'check', false]);
25+
}
26+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script>
2+
import Child from './Child.svelte';
3+
4+
let a = $state(0);
5+
const a_bindings = [
6+
() => a,
7+
(v) => {
8+
console.log('a', v);
9+
a = v;
10+
}
11+
]
12+
let check = $state(true);
13+
const check_bindings = [
14+
() => check,
15+
(v) => {
16+
console.log('check', v);
17+
check = v;
18+
}
19+
]
20+
</script>
21+
22+
<button onclick={() => a++}>a: {a}</button>
23+
24+
<Child
25+
bind:a={...a_bindings}
26+
/>
27+
28+
<div>
29+
<input type="checkbox" bind:checked={...check_bindings} />
30+
</div>

0 commit comments

Comments
 (0)