allow rest props on $props.bindable() and add related dev time validation, closes #10711

props-bindable
Simon Holthausen 6 months ago
parent 84e2dd3130
commit 1d818380dd

@ -180,7 +180,6 @@ const runes = {
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
'invalid-props-pattern': () =>
`$props() assignment must not contain nested properties or computed keys`,
'invalid-props-rest-element': () => `Cannot use ...rest parameter with $props.bindable()`,
'invalid-props-location': () =>
`$props() can only be used at the top level of components as a variable declaration initializer`,
/** @param {string} rune */

@ -1095,8 +1095,6 @@ export const validation_runes = merge(validation, a11y_validators, {
if (value.type !== 'Identifier') {
error(property, 'invalid-props-pattern');
}
} else if (rune === '$props.bindable') {
error(property, 'invalid-props-rest-element');
}
}
}

@ -224,16 +224,12 @@ export const javascript_visitors_runes = {
}
} else {
// RestElement
declarations.push(
b.declarator(
property.argument,
b.call(
'$.rest_props',
b.id('$$props'),
b.array(seen.map((name) => b.literal(name)))
)
)
);
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (rune === '$props.bindable') {
args.push(b.literal(true));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}

@ -36,21 +36,39 @@ export function update_pre_prop(fn, d = 1) {
/**
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol> }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, p: boolean }>}}
*/
const rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
return target.props[key];
},
set(target, key, value) {
if (target.exclude.includes(key) || !(key in target.props)) return false;
if (DEV) {
if (!target.p) {
throw new Error(
`Cannot set read-only property '${String(key)}' of rest element of $props(). Only rest elements from $props.bindable() can be written to.'`
);
} else if (!get_descriptor(target.props, key)?.set) {
throw new Error(
`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.`
);
}
}
target.props[key] = value;
return true;
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (key in target.props) {
return {
enumerable: true,
configurable: true,
value: target.props[key]
};
return target.p
? get_descriptor(target.props, key)
: {
enumerable: true,
configurable: true,
value: target.props[key]
};
}
},
has(target, key) {
@ -65,10 +83,11 @@ const rest_props_handler = {
/**
* @param {Record<string, unknown>} props
* @param {string[]} rest
* @param {boolean} [preserve_setters]
* @returns {Record<string, unknown>}
*/
export function rest_props(props, rest) {
return new Proxy({ props, exclude: rest }, rest_props_handler);
export function rest_props(props, rest, preserve_setters = false) {
return new Proxy({ props, exclude: rest, p: preserve_setters }, rest_props_handler);
}
/**

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-props-rest-element',
message: 'Cannot use ...rest parameter with $props.bindable()',
position: [53, 62]
}
});

@ -1,4 +0,0 @@
<script>
let { foo, ...ok } = $props();
let { bar, ...not_ok } = $props.bindable();
</script>

@ -0,0 +1,5 @@
<script>
let { ...rest } = $props.bindable();
</script>
<button on:click={() => rest.count++}>{rest.count}</button>

@ -0,0 +1,49 @@
import { test } from '../../test';
let failed_too_soon = true;
export default test({
html: `
<p>0 0 0</p>
<button>0</button>
<button>0</button>
<button>0</button>
`,
before_test() {
failed_too_soon = true;
},
async test({ assert, target }) {
const [b1, b2, b3] = target.querySelectorAll('button');
b1.click();
b2.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<p>1 1 0</p>
<button>1</button>
<button>1</button>
<button>0</button>
`
);
failed_too_soon = false;
b3.click();
await Promise.resolve();
},
test_ssr() {
failed_too_soon = false;
},
after_test() {
if (failed_too_soon) {
throw new Error('Test failed too soon');
}
},
runtime_error:
"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."
});

@ -0,0 +1,13 @@
<script>
import Counter from './Counter.svelte';
let bound = $state(0);
let bound_nested = $state({count: 0});
let unbound = $state(0);
</script>
<p>{bound} {bound_nested.count} {unbound}</p>
<Counter bind:count={bound} />
<Counter bind:count={bound_nested.count} />
<Counter count={unbound} />
Loading…
Cancel
Save