Ensure binding from legacy component passed to runes component updates correctly. This is done by also using the `prop(..)` variant for a property if it's mutated in runes mode, and then figuring out at runtime whether or not the parent should be notified or not

fixes #12032
pull/12123/head
Simon Holthausen 2 years ago
parent 7295facb6c
commit 48eef0d5b4

@ -363,7 +363,12 @@ export function client_component(source, analysis, options) {
}
if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
const to_remove = [
b.literal('children'),
b.literal('$$slots'),
b.literal('$$events'),
b.literal('$$legacy')
];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}

@ -89,7 +89,7 @@ export function serialize_get_binding(node, state) {
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (!state.analysis.runes || binding.reassigned || binding.initial) {
if (!state.analysis.runes || binding.reassigned || binding.initial || binding.mutated) {
return b.call(node);
}
@ -391,7 +391,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
),
b.call('$.untrack', b.id('$' + left_name))
);
} else if (!state.analysis.runes) {
} else if (!state.analysis.runes || (binding.mutated && binding.kind === 'bindable_prop')) {
if (binding.kind === 'bindable_prop') {
return b.call(
left,
@ -402,7 +402,8 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
value
),
b.call(left)
])
]),
b.true
);
} else {
return b.call(
@ -539,6 +540,7 @@ function get_hoistable_params(node, context) {
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
!binding.reassigned &&
!binding.mutated && // this can be removed once legacy mode is gone
binding.initial === null &&
!context.state.analysis.accessors
) {
@ -602,7 +604,9 @@ export function get_prop_source(binding, state, name, initial) {
if (
state.analysis.accessors ||
(state.analysis.immutable ? binding.reassigned : binding.mutated)
(state.analysis.immutable
? binding.reassigned || (state.analysis.runes && binding.mutated)
: binding.mutated)
) {
flags |= PROPS_IS_UPDATED;
}

@ -240,7 +240,11 @@ export const javascript_visitors_runes = {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = [];
const seen = ['$$slots', '$$events', '$$legacy'];
if (state.analysis.custom_element) {
seen.push('$$host');
}
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
@ -264,7 +268,9 @@ export const javascript_visitors_runes = {
initial = b.call('$.proxy', initial);
}
if (binding.reassigned || state.analysis.accessors || initial) {
// Until legacy mode is gone, we also need to use the prop source when only mutated is true,
// because the parent could be a legacy component which needs coarse-grained reactivity
if (binding.reassigned || binding.mutated || state.analysis.accessors || initial) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {

@ -900,6 +900,10 @@ function serialize_inline_component(node, component_name, context) {
push_prop(b.init('$$slots', b.object(serialized_slots)));
}
if (!context.state.analysis.runes) {
push_prop(b.init('$$legacy', b.true));
}
const props_expression =
props_and_spreads.length === 0 ||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0]))

@ -267,9 +267,16 @@ export function prop(props, key, flags, fallback) {
// intermediate mode — prop is written to, but the parent component had
// `bind:foo` which means we can just call `$$props.foo = value` directly
if (setter) {
return function (/** @type {V} */ value) {
if (arguments.length === 1) {
/** @type {Function} */ (setter)(value);
var legacy_parent = props.$$legacy;
return function (/** @type {V} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
// In that case the state proxy (if it exists) should take care of the notification.
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
// has changed because the parent will not be able to detect the change otherwise.
if (!runes || !mutation || legacy_parent) {
/** @type {Function} */ (setter)(value);
}
return value;
} else {
return getter();

@ -0,0 +1,9 @@
<script>
let { object = $bindable(), primitive = $bindable() } = $props();
</script>
{#if primitive}
<button onclick={() => (primitive = 'bar')}>{primitive}</button>
{:else}
<button onclick={() => (object.value = 'bar')}>{object.value}</button>
{/if}

@ -0,0 +1,9 @@
<script>
let { object = $bindable({}), primitive = $bindable('') } = $props();
</script>
{#if primitive}
<button onclick={() => (primitive = 'bar')}>{primitive}</button>
{:else}
<button onclick={() => (object.value = 'bar')}>{object.value}</button>
{/if}

@ -0,0 +1,24 @@
<svelte:options runes={false} />
<script>
import Component1 from './Component1.svelte';
import Component2 from './Component2.svelte';
let object1 = { value: 'foo' };
let object2 = { value: 'foo' };
let primitive1 = 'foo';
let primitive2 = 'foo';
</script>
{object1.value}
<Component1 bind:object={object1} />
{object2.value}
<Component2 bind:object={object2} />
{primitive1}
<Component1 bind:primitive={primitive1} />
{primitive2}
<Component2 bind:primitive={primitive2} />

@ -0,0 +1,39 @@
<script>
import Component1 from './Component1.svelte';
import Component2 from './Component2.svelte';
let object1 = $state({ value: 'foo' });
let object2 = $state({ value: 'foo' });
class Frozen {
constructor(value) {
this.value = value;
}
}
let object3 = $state(new Frozen('foo'));
let object4 = $state(new Frozen('foo'));
let primitive1 = $state('foo');
let primitive2 = $state('foo');
</script>
{object1.value}
<Component1 bind:object={object1} />
{object2.value}
<Component2 bind:object={object2} />
<!-- force them into a different render effect so they don't coincidently update with the others -->
{#if true}
{object3.value}
<Component1 bind:object={object3} />
{object4.value}
<Component2 bind:object={object4} />
{/if}
{primitive1}
<Component1 bind:primitive={primitive1} />
{primitive2}
<Component2 bind:primitive={primitive2} />

@ -0,0 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
const buttons = target.querySelectorAll('button');
for (const button of buttons) {
await button.click();
flushSync();
}
flushSync();
assert.htmlEqual(
target.innerHTML,
`
bar <button>bar</button> bar <button>bar</button> bar <button>bar</button> bar <button>bar</button>
<hr>
bar <button>bar</button> bar <button>bar</button> foo <button>foo</button> foo <button>foo</button> bar <button>bar</button> bar <button>bar</button>
`
);
}
});

@ -0,0 +1,10 @@
<script>
import Legacy from './Legacy.svelte';
import Runes from './Runes.svelte';
</script>
<Legacy />
<hr />
<Runes />

@ -5,6 +5,6 @@ export default function Bind_this($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
$.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo);
$.append($$anchor, fragment);
}
Loading…
Cancel
Save