add $props.bindable()

props-bindable
Simon Holthausen 10 months ago
parent c35f0c16af
commit 001a8314fa

@ -422,7 +422,7 @@ export function analyze_component(root, options) {
options,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
parent_element: null,
has_props_rune: false,
has_props_rune: [false, false],
component_slots: new Set(),
expression: null,
private_derived_state: [],
@ -446,7 +446,7 @@ export function analyze_component(root, options) {
analysis,
options,
parent_element: null,
has_props_rune: false,
has_props_rune: [false, false],
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
instance_scope: instance.scope,
reactive_statement: null,
@ -854,7 +854,8 @@ const runes_scope_tweaker = {
rune !== '$state.frozen' &&
rune !== '$derived' &&
rune !== '$derived.by' &&
rune !== '$props'
rune !== '$props' &&
rune !== '$props.bindable'
)
return;
@ -873,7 +874,7 @@ const runes_scope_tweaker = {
: 'prop';
}
if (rune === '$props') {
if (rune === '$props' || rune === '$props.bindable') {
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;

@ -15,7 +15,7 @@ export interface AnalysisState {
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
parent_element: string | null;
has_props_rune: boolean;
has_props_rune: [props: boolean, bindings: boolean];
/** Which slots the current parent component has */
component_slots: Set<string>;
/** The current {expression}, if any */

@ -775,12 +775,17 @@ function validate_call_expression(node, scope, path) {
const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1));
if (rune === '$props') {
if (rune === '$props' || rune === '$props.bindable') {
if (parent.type === 'VariableDeclarator') return;
error(node, 'invalid-props-location');
}
if (rune === '$state' || rune === '$derived' || rune === '$derived.by') {
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by'
) {
if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune);
@ -871,7 +876,7 @@ export const validation_runes_js = {
error(node, 'invalid-rune-args-length', rune, [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
} else if (rune === '$props') {
} else if (rune === '$props' || rune === '$props.bindable') {
error(node, 'invalid-props-location');
}
},
@ -1054,15 +1059,18 @@ export const validation_runes = merge(validation, a11y_validators, {
error(node, 'invalid-rune-args-length', rune, [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
} else if (rune === '$props') {
if (state.has_props_rune) {
} else if (rune === '$props' || rune === '$props.bindable') {
if (
(rune === '$props' && state.has_props_rune[0]) ||
(rune === '$props.bindable' && state.has_props_rune[1])
) {
error(node, 'duplicate-props-rune');
}
state.has_props_rune = true;
state.has_props_rune[rune === '$props' ? 0 : 1] = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', '$props', [0]);
error(node, 'invalid-rune-args-length', rune, [0]);
}
if (node.id.type !== 'ObjectPattern') {

@ -192,7 +192,7 @@ export const javascript_visitors_runes = {
continue;
}
if (rune === '$props') {
if (rune === '$props' || rune === '$props.bindable') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */

@ -689,7 +689,7 @@ const javascript_visitors_runes = {
continue;
}
if (rune === '$props') {
if (rune === '$props' || rune === '$props.bindable') {
declarations.push(b.declarator(declarator.id, b.id('$$props')));
continue;
}

@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',
'$props',
'$props.bindable',
'$derived',
'$derived.by',
'$effect',

@ -1253,22 +1253,33 @@ export function unwrap(value) {
}
if (DEV) {
/** @param {string} rune */
function throw_rune_error(rune) {
/**
* @param {string} rune
* @param {string[]} [variants]
*/
function throw_rune_error(rune, variants = []) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
// @ts-ignore
globalThis[rune] = () => {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
throw new Error(`${rune} is only available inside .svelte and .svelte.js/ts files`);
throw new Error(`${rune}() is only available inside .svelte and .svelte.js/ts files`);
};
for (const variant of variants) {
// @ts-ignore
globalThis[rune][variant] = () => {
throw new Error(
`${rune}.${variant}() is only available inside .svelte and .svelte.js/ts files`
);
};
}
}
}
throw_rune_error('$state');
throw_rune_error('$effect');
throw_rune_error('$derived');
throw_rune_error('$state', ['frozen']);
throw_rune_error('$effect', ['pre', 'root', 'active']);
throw_rune_error('$derived', ['by']);
throw_rune_error('$inspect');
throw_rune_error('$props');
throw_rune_error('$props', ['bindable']);
}
/**

@ -175,10 +175,25 @@ declare namespace $effect {
* let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props();
* ```
*
* Props declared with `$props()` cannot be used with `bind:`, use `$props.bindable()` for these instead.
*
* https://svelte-5-preview.vercel.app/docs/runes#$props
*/
declare function $props(): any;
declare namespace $props {
/**
* Declares the props that a component accepts and which consumers can `bind:` to. Example:
*
* ```ts
* let { optionalProp, requiredProp }: { optionalProp?: number; requiredProps: string } = $props.bindable();
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$props
*/
function bindable(): any;
}
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:
*

@ -2462,10 +2462,25 @@ declare namespace $effect {
* let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props();
* ```
*
* Props declared with `$props()` cannot be used with `bind:`, use `$props.bindable()` for these instead.
*
* https://svelte-5-preview.vercel.app/docs/runes#$props
*/
declare function $props(): any;
declare namespace $props {
/**
* Declares the props that a component accepts and which consumers can `bind:` to. Example:
*
* ```ts
* let { optionalProp, requiredProp }: { optionalProp?: number; requiredProps: string } = $props.bindable();
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$props
*/
function bindable(): any;
}
/**
* Inspects one or more values whenever they, or the properties they contain, change. Example:
*

@ -220,6 +220,7 @@
boost: 5
}),
{ label: '$state.frozen', type: 'keyword', boost: 4 },
{ label: '$props.bindable', type: 'keyword', boost: 4 },
snip('$effect.root(() => {\n\t${}\n});', {
label: '$effect.root',
type: 'keyword',

@ -471,7 +471,7 @@ let { a, b, c, ...everythingElse }: MyProps = $props();
>
> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring.
Props cannot be mutated, unless the parent component uses `bind:`. During development, attempts to mutate props will result in an error.
Props declared with `$props()` cannot be mutated, and you cannot `bind:` to them in the parent. To declare props as bindable, use [`$props.bindable()`](#propsbindable).
### What this replaces
@ -479,6 +479,10 @@ Props cannot be mutated, unless the parent component uses `bind:`. During develo
Note that you can still use `export const` and `export function` to expose things to users of your component (if they're using `bind:this`, for example).
### `$props.bindable()`
To declare props as bindable, use `$props.bindable()`. Besides using them as regular props, the parent can then also `bind:` to these props. Works exactly like `$props()`. During development, attempts to mutate these props will result in a warning, unless the parent did `bind:` them.
## `$inspect`
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its

Loading…
Cancel
Save