Skip to content

Commit

Permalink
fix: ensure bindings always take precedence over spreads (#14575)
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm authored Dec 5, 2024
1 parent 6a6b4ec commit ca67aa1
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-berries-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure bindings always take precedence over spreads
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js';
export function build_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];

/** @type {ExpressionStatement[]} */
const lets = [];
Expand Down Expand Up @@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context.

/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};

if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}

Expand Down Expand Up @@ -202,22 +213,27 @@ export function build_component(node, component_name, context, anchor = context.
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)])
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)]));
push_prop(b.get(attribute.name, [b.return(expression)]), true);
}

const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
true
);
}
}
}

delayed_props.forEach((fn) => fn());

if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js';
export function build_inline_component(node, expression, context) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
const delayed_props = [];

/** @type {Property[]} */
const custom_css_props = [];
Expand Down Expand Up @@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) {

/**
* @param {Property} prop
* @param {boolean} [delay]
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
function push_prop(prop, delay = false) {
const do_push = () => {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
};

if (delay) {
delayed_props.push(do_push);
} else {
do_push();
}
}

Expand All @@ -81,11 +92,12 @@ 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') {
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
]),
true
);
push_prop(
b.set(attribute.name, [
Expand All @@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) {
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
]),
true
);
}
}

delayed_props.forEach((fn) => fn());

/** @type {Statement[]} */
const snippet_declarations = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test } from '../../test';

export default test({
ssrHtml: `<input value="foo">`,

test({ assert, target }) {
assert.equal(target.querySelector('input')?.value, 'foo');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let { value = $bindable(), ...properties } = $props();
</script>

<input bind:value {...properties} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import Button from './input.svelte';
let value = $state('foo');
const props = $state({
value: 'bar'
});
</script>

<Button bind:value {...props} />

0 comments on commit ca67aa1

Please sign in to comment.