Skip to content

Commit 771b1e8

Browse files
trueadmRich-Harris
andauthored
fix: enable bound store props in runes mode components (#13887)
* fix: enable bound store props in runes mode components * add some JSDoc, since it could be a headscratcher for future us * make it clear that this is specifically about bindings * skip intermediate value * tweak other names too --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 4cf2d4a commit 771b1e8

File tree

9 files changed

+108
-5
lines changed

9 files changed

+108
-5
lines changed

.changeset/cool-clocks-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: enable bound store props in runes mode components

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,17 @@ export function build_component(node, component_name, context, anchor = context.
188188
);
189189
}
190190

191-
push_prop(b.get(attribute.name, [b.return(expression)]));
191+
const is_store_sub =
192+
attribute.expression.type === 'Identifier' &&
193+
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
194+
195+
if (is_store_sub) {
196+
push_prop(
197+
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
198+
);
199+
} else {
200+
push_prop(b.get(attribute.name, [b.return(expression)]));
201+
}
192202

193203
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
194204
push_prop(

packages/svelte/src/internal/client/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ export {
121121
store_set,
122122
store_unsub,
123123
update_pre_store,
124-
update_store
124+
update_store,
125+
mark_store_binding
125126
} from './reactivity/store.js';
126127
export { set_text } from './render.js';
127128
export {

packages/svelte/src/internal/client/reactivity/props.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { safe_equals } from './equality.js';
2323
import * as e from '../errors.js';
2424
import { BRANCH_EFFECT, DESTROYED, LEGACY_DERIVED_PROP, ROOT_EFFECT } from '../constants.js';
2525
import { proxy } from '../proxy.js';
26+
import { capture_store_binding } from './store.js';
2627

2728
/**
2829
* @param {((value?: number) => number)} fn
@@ -273,8 +274,14 @@ export function prop(props, key, flags, fallback) {
273274
var runes = (flags & PROPS_IS_RUNES) !== 0;
274275
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
275276
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
277+
var is_store_sub = false;
278+
var prop_value;
276279

277-
var prop_value = /** @type {V} */ (props[key]);
280+
if (bindable) {
281+
[prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
282+
} else {
283+
prop_value = /** @type {V} */ (props[key]);
284+
}
278285
var setter = get_descriptor(props, key)?.set;
279286

280287
var fallback_value = /** @type {V} */ (fallback);
@@ -343,7 +350,7 @@ export function prop(props, key, flags, fallback) {
343350
// In that case the state proxy (if it exists) should take care of the notification.
344351
// If the parent is not in runes mode, we need to notify on mutation, too, that the prop
345352
// has changed because the parent will not be able to detect the change otherwise.
346-
if (!runes || !mutation || legacy_parent) {
353+
if (!runes || !mutation || legacy_parent || is_store_sub) {
347354
/** @type {Function} */ (setter)(mutation ? getter() : value);
348355
}
349356
return value;

packages/svelte/src/internal/client/reactivity/store.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import { get } from '../runtime.js';
66
import { teardown } from './effects.js';
77
import { mutable_source, set } from './sources.js';
88

9+
/**
10+
* Whether or not the prop currently being read is a store binding, as in
11+
* `<Child bind:x={$y} />`. If it is, we treat the prop as mutable even in
12+
* runes mode, and skip `binding_property_non_reactive` validation
13+
*/
14+
let is_store_binding = false;
15+
916
/**
1017
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy
1118
* signal that will be updated when the store is. The store references container is needed to
@@ -146,3 +153,29 @@ export function update_pre_store(store, store_value, d = 1) {
146153
store.set(value);
147154
return value;
148155
}
156+
157+
/**
158+
* Called inside prop getters to communicate that the prop is a store binding
159+
*/
160+
export function mark_store_binding() {
161+
is_store_binding = true;
162+
}
163+
164+
/**
165+
* Returns a tuple that indicates whether `fn()` reads a prop that is a store binding.
166+
* Used to prevent `binding_property_non_reactive` validation false positives and
167+
* ensure that these props are treated as mutable even in runes mode
168+
* @template T
169+
* @param {() => T} fn
170+
* @returns {[T, boolean]}
171+
*/
172+
export function capture_store_binding(fn) {
173+
var previous_is_store_binding = is_store_binding;
174+
175+
try {
176+
is_store_binding = false;
177+
return [fn(), is_store_binding];
178+
} finally {
179+
is_store_binding = previous_is_store_binding;
180+
}
181+
}

packages/svelte/src/internal/client/validate.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as e from './errors.js';
44
import { FILENAME } from '../../constants.js';
55
import { render_effect } from './reactivity/effects.js';
66
import * as w from './warnings.js';
7+
import { capture_store_binding } from './reactivity/store.js';
78

89
/** regex of all html void element names */
910
const void_element_names =
@@ -84,7 +85,10 @@ export function validate_binding(binding, get_object, get_property, line, column
8485
render_effect(() => {
8586
if (warned) return;
8687

87-
var object = get_object();
88+
var [object, is_store_sub] = capture_store_binding(get_object);
89+
90+
if (is_store_sub) return;
91+
8892
var property = get_property();
8993

9094
var ran = false;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { form = $bindable() } = $props();
3+
</script>
4+
5+
<p>
6+
<input type="number" bind:value={form.count} />
7+
</p>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
import { ok } from 'assert';
4+
5+
export default test({
6+
compileOptions: {
7+
dev: true
8+
},
9+
10+
html: `<p><input type="number"></p>\n{"count":0}`,
11+
ssrHtml: `<p><input type="number" value="0"></p>\n{"count":0}`,
12+
13+
test({ assert, target }) {
14+
const input = target.querySelector('input');
15+
ok(input);
16+
const inputEvent = new window.InputEvent('input');
17+
18+
input.value = '10';
19+
input.dispatchEvent(inputEvent);
20+
21+
flushSync();
22+
23+
assert.htmlEqual(target.innerHTML, `<p><input type="number"></p>\n{"count":10}`);
24+
}
25+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import { writable } from 'svelte/store';
3+
import Child from './Child.svelte';
4+
5+
let form = writable({
6+
count: 0
7+
});
8+
</script>
9+
10+
<Child bind:form={$form} />
11+
{JSON.stringify($form)}

0 commit comments

Comments
 (0)