Skip to content

Commit 1d81838

Browse files
committed
allow rest props on $props.bindable() and add related dev time validation, closes #10711
1 parent 84e2dd3 commit 1d81838

File tree

9 files changed

+100
-34
lines changed

9 files changed

+100
-34
lines changed

packages/svelte/src/compiler/errors.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ const runes = {
180180
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
181181
'invalid-props-pattern': () =>
182182
`$props() assignment must not contain nested properties or computed keys`,
183-
'invalid-props-rest-element': () => `Cannot use ...rest parameter with $props.bindable()`,
184183
'invalid-props-location': () =>
185184
`$props() can only be used at the top level of components as a variable declaration initializer`,
186185
/** @param {string} rune */

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,8 +1095,6 @@ export const validation_runes = merge(validation, a11y_validators, {
10951095
if (value.type !== 'Identifier') {
10961096
error(property, 'invalid-props-pattern');
10971097
}
1098-
} else if (rune === '$props.bindable') {
1099-
error(property, 'invalid-props-rest-element');
11001098
}
11011099
}
11021100
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,12 @@ export const javascript_visitors_runes = {
224224
}
225225
} else {
226226
// RestElement
227-
declarations.push(
228-
b.declarator(
229-
property.argument,
230-
b.call(
231-
'$.rest_props',
232-
b.id('$$props'),
233-
b.array(seen.map((name) => b.literal(name)))
234-
)
235-
)
236-
);
227+
/** @type {import('estree').Expression[]} */
228+
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
229+
if (rune === '$props.bindable') {
230+
args.push(b.literal(true));
231+
}
232+
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
237233
}
238234
}
239235

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,39 @@ export function update_pre_prop(fn, d = 1) {
3636
/**
3737
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
3838
* Is passed the full `$$props` object and excludes the named props.
39-
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol> }>}}
39+
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, p: boolean }>}}
4040
*/
4141
const rest_props_handler = {
4242
get(target, key) {
4343
if (target.exclude.includes(key)) return;
4444
return target.props[key];
4545
},
46+
set(target, key, value) {
47+
if (target.exclude.includes(key) || !(key in target.props)) return false;
48+
if (DEV) {
49+
if (!target.p) {
50+
throw new Error(
51+
`Cannot set read-only property '${String(key)}' of rest element of $props(). Only rest elements from $props.bindable() can be written to.'`
52+
);
53+
} else if (!get_descriptor(target.props, key)?.set) {
54+
throw new Error(
55+
`Cannot write to property '${String(key)}' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component.`
56+
);
57+
}
58+
}
59+
target.props[key] = value;
60+
return true;
61+
},
4662
getOwnPropertyDescriptor(target, key) {
4763
if (target.exclude.includes(key)) return;
4864
if (key in target.props) {
49-
return {
50-
enumerable: true,
51-
configurable: true,
52-
value: target.props[key]
53-
};
65+
return target.p
66+
? get_descriptor(target.props, key)
67+
: {
68+
enumerable: true,
69+
configurable: true,
70+
value: target.props[key]
71+
};
5472
}
5573
},
5674
has(target, key) {
@@ -65,10 +83,11 @@ const rest_props_handler = {
6583
/**
6684
* @param {Record<string, unknown>} props
6785
* @param {string[]} rest
86+
* @param {boolean} [preserve_setters]
6887
* @returns {Record<string, unknown>}
6988
*/
70-
export function rest_props(props, rest) {
71-
return new Proxy({ props, exclude: rest }, rest_props_handler);
89+
export function rest_props(props, rest, preserve_setters = false) {
90+
return new Proxy({ props, exclude: rest, p: preserve_setters }, rest_props_handler);
7291
}
7392

7493
/**

packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { ...rest } = $props.bindable();
3+
</script>
4+
5+
<button on:click={() => rest.count++}>{rest.count}</button>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test } from '../../test';
2+
3+
let failed_too_soon = true;
4+
5+
export default test({
6+
html: `
7+
<p>0 0 0</p>
8+
<button>0</button>
9+
<button>0</button>
10+
<button>0</button>
11+
`,
12+
13+
before_test() {
14+
failed_too_soon = true;
15+
},
16+
async test({ assert, target }) {
17+
const [b1, b2, b3] = target.querySelectorAll('button');
18+
19+
b1.click();
20+
b2.click();
21+
await Promise.resolve();
22+
23+
assert.htmlEqual(
24+
target.innerHTML,
25+
`
26+
<p>1 1 0</p>
27+
<button>1</button>
28+
<button>1</button>
29+
<button>0</button>
30+
`
31+
);
32+
33+
failed_too_soon = false;
34+
35+
b3.click();
36+
await Promise.resolve();
37+
},
38+
test_ssr() {
39+
failed_too_soon = false;
40+
},
41+
after_test() {
42+
if (failed_too_soon) {
43+
throw new Error('Test failed too soon');
44+
}
45+
},
46+
47+
runtime_error:
48+
"Cannot write to property 'count' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component."
49+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import Counter from './Counter.svelte';
3+
4+
let bound = $state(0);
5+
let bound_nested = $state({count: 0});
6+
let unbound = $state(0);
7+
</script>
8+
9+
<p>{bound} {bound_nested.count} {unbound}</p>
10+
11+
<Counter bind:count={bound} />
12+
<Counter bind:count={bound_nested.count} />
13+
<Counter count={unbound} />

0 commit comments

Comments
 (0)