diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 7feeebdbbc..fc2d5b0833 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement, SpreadElement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; @@ -8,6 +8,7 @@ import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '.. import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -48,7 +49,7 @@ export function build_component(node, component_name, context) { /** @type {Property[]} */ const custom_css_props = []; - /** @type {Identifier | MemberExpression | SequenceExpression | null} */ + /** @type {Identifier | MemberExpression | SequenceExpression | SpreadElement | null} */ let bind_this = null; /** @type {ExpressionStatement[]} */ @@ -196,84 +197,100 @@ export function build_component(node, component_name, context) { push_prop(b.init(attribute.name, value)); } } else if (attribute.type === 'BindDirective') { - const expression = /** @type {Expression} */ (context.visit(attribute.expression)); + if (attribute.expression.type === 'SpreadElement') { + const { get, set } = init_spread_bindings(attribute.expression, context); - if ( - dev && - attribute.name !== 'this' && - !is_ignored(node, 'ownership_invalid_binding') && - // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation - attribute.expression.type !== 'SequenceExpression' - ) { - const left = object(attribute.expression); - const binding = left && context.state.scope.get(left.name); - - if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') { - context.state.analysis.needs_mutation_validation = true; - binding_initializers.push( - b.stmt( - b.call( - '$$ownership_validator.binding', - b.literal(binding.node.name), - b.id(is_component_dynamic ? intermediate_name : component_name), - b.thunk(expression) - ) - ) - ); - } - } - - if (expression.type === 'SequenceExpression') { if (attribute.name === 'this') { bind_this = attribute.expression; } else { - const [get, set] = expression.expressions; - const get_id = b.id(context.state.scope.generate('bind_get')); - const set_id = b.id(context.state.scope.generate('bind_set')); - - context.state.init.push(b.var(get_id, get)); - context.state.init.push(b.var(set_id, set)); - - push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); - push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + push_prop(b.get(attribute.name, [b.return(b.call(get))]), true); + push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true); } } else { + const expression = /** @type {Expression} */ (context.visit(attribute.expression)); + if ( dev && - expression.type === 'MemberExpression' && - context.state.analysis.runes && - !is_ignored(node, 'binding_property_non_reactive') + attribute.name !== 'this' && + !is_ignored(node, 'ownership_invalid_binding') && + // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation + attribute.expression.type !== 'SequenceExpression' ) { - validate_binding(context.state, attribute, expression); + const left = object(attribute.expression); + const binding = left && context.state.scope.get(left.name); + + if (binding?.kind === 'bindable_prop' || binding?.kind === 'prop') { + context.state.analysis.needs_mutation_validation = true; + binding_initializers.push( + b.stmt( + b.call( + '$$ownership_validator.binding', + b.literal(binding.node.name), + b.id(is_component_dynamic ? intermediate_name : component_name), + b.thunk(expression) + ) + ) + ); + } } - if (attribute.name === 'this') { - bind_this = attribute.expression; + if (expression.type === 'SequenceExpression') { + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const [get, set] = expression.expressions; + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } } else { - const is_store_sub = - attribute.expression.type === 'Identifier' && - context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + if ( + dev && + expression.type === 'MemberExpression' && + context.state.analysis.runes && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, attribute, expression); + } + + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const is_store_sub = + attribute.expression.type === 'Identifier' && + context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + if (is_store_sub) { + push_prop( + b.get(attribute.name, [ + b.stmt(b.call('$.mark_store_binding')), + b.return(expression) + ]), + true + ); + } else { + push_prop(b.get(attribute.name, [b.return(expression)]), true); + } + + const assignment = b.assignment( + '=', + /** @type {Pattern} */ (attribute.expression), + b.id('$$value') + ); - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - if (is_store_sub) { push_prop( - b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + b.set(attribute.name, [ + b.stmt(/** @type {Expression} */ (context.visit(assignment))) + ]), true ); - } else { - push_prop(b.get(attribute.name, [b.return(expression)]), true); } - - const assignment = b.assignment( - '=', - /** @type {Pattern} */ (attribute.expression), - b.id('$$value') - ); - - push_prop( - b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), - true - ); } } } else if (attribute.type === 'AttachTag') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 9bccf9e05e..a4b3439eae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -5,6 +5,7 @@ import { empty_comment, build_attribute_value } from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -93,7 +94,17 @@ export function build_inline_component(node, expression, context) { const value = build_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - if (attribute.expression.type === 'SequenceExpression') { + if (attribute.expression.type === 'SpreadElement') { + const { get, set } = init_spread_bindings(attribute.expression, context); + + push_prop(b.get(attribute.name, [b.return(b.call(get))])); + push_prop( + b.set(attribute.name, [ + b.stmt(b.call(set, b.id('$$value'))), + b.stmt(b.assignment('=', b.id('$$settled'), b.false)) + ]) + ); + } else if (attribute.expression.type === 'SequenceExpression') { const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) .expressions; const get_id = b.id(context.state.scope.generate('bind_get')); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte new file mode 100644 index 0000000000..5c8fd19fa4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js new file mode 100644 index 0000000000..dd5c387405 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js @@ -0,0 +1,26 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { assert_ok } from '../../../suite'; + +export default test({ + async test({ assert, target, logs }) { + const [input, checkbox] = target.querySelectorAll('input'); + + input.value = '2'; + input.dispatchEvent(new window.Event('input')); + + flushSync(); + + assert.htmlEqual( + target.innerHTML, + `
` + ); + + assert.deepEqual(logs, ['b', '2', 'a', '2']); + + flushSync(() => { + checkbox.click(); + }); + assert.deepEqual(logs, ['b', '2', 'a', '2', 'check', false]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte new file mode 100644 index 0000000000..7dc2aaf317 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte @@ -0,0 +1,30 @@ + + + + + + +
+ +