Skip to content

Commit 5771b45

Browse files
trueadmRich-Harrisdummdidumm
authored
feat: add support for bind getter/setters (#14307)
* feat: add support for bind getters/setters * different direction * oops * oops * build * add changeset and tests * move validation * add comment * build * bind:group error * simpler to just keep it as a SequenceExpression * fix * lint * fix * move validation to visitor * fix * no longer needed * fix * parser changes are no longer needed * simplify * simplify * update messages * docs --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent 1a0b822 commit 5771b45

File tree

23 files changed

+471
-217
lines changed

23 files changed

+471
-217
lines changed

.changeset/slimy-donkeys-hang.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+
feat: add support for bind getters/setters

documentation/docs/03-template-syntax/11-bind.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,34 @@ The general syntax is `bind:property={expression}`, where `expression` is an _lv
1212
<input bind:value />
1313
```
1414

15+
1516
Svelte creates an event listener that updates the bound value. If an element already has a listener for the same event, that listener will be fired before the bound value is updated.
1617

1718
Most bindings are _two-way_, meaning that changes to the value will affect the element and vice versa. A few bindings are _readonly_, meaning that changing their value will have no effect on the element.
1819

20+
## Function bindings
21+
22+
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
23+
24+
```svelte
25+
<input bind:value={
26+
() => value,
27+
(v) => value = v.toLowerCase()}
28+
/>
29+
```
30+
31+
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
32+
33+
```svelte
34+
<div
35+
bind:clientWidth={null, redraw}
36+
bind:clientHeight={null, redraw}
37+
>...</div>
38+
```
39+
40+
> [!NOTE]
41+
> Function bindings are available in Svelte 5.9.0 and newer.
42+
1943
## `<input bind:value>`
2044

2145
A `bind:value` directive on an `<input>` element binds the input's `value` property:

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,16 @@ Sequence expressions are not allowed as attribute/directive values in runes mode
7878
Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression
7979
```
8080

81+
### bind_group_invalid_expression
82+
83+
```
84+
`bind:group` can only bind to an Identifier or MemberExpression
85+
```
86+
8187
### bind_invalid_expression
8288

8389
```
84-
Can only bind to an Identifier or MemberExpression
90+
Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
8591
```
8692

8793
### bind_invalid_name
@@ -94,6 +100,12 @@ Can only bind to an Identifier or MemberExpression
94100
`bind:%name%` is not a valid binding. %explanation%
95101
```
96102

103+
### bind_invalid_parens
104+
105+
```
106+
`bind:%name%={get, set}` must not have surrounding parentheses
107+
```
108+
97109
### bind_invalid_target
98110

99111
```

packages/svelte/messages/compile-errors/template.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,24 @@
5050

5151
> Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression
5252
53+
## bind_group_invalid_expression
54+
55+
> `bind:group` can only bind to an Identifier or MemberExpression
56+
5357
## bind_invalid_expression
5458

55-
> Can only bind to an Identifier or MemberExpression
59+
> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
5660
5761
## bind_invalid_name
5862

5963
> `bind:%name%` is not a valid binding
6064
6165
> `bind:%name%` is not a valid binding. %explanation%
6266
67+
## bind_invalid_parens
68+
69+
> `bind:%name%={get, set}` must not have surrounding parentheses
70+
6371
## bind_invalid_target
6472

6573
> `bind:%name%` can only be used with %elements%

packages/svelte/src/compiler/errors.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,12 +716,21 @@ export function attribute_unquoted_sequence(node) {
716716
}
717717

718718
/**
719-
* Can only bind to an Identifier or MemberExpression
719+
* `bind:group` can only bind to an Identifier or MemberExpression
720+
* @param {null | number | NodeLike} node
721+
* @returns {never}
722+
*/
723+
export function bind_group_invalid_expression(node) {
724+
e(node, "bind_group_invalid_expression", "`bind:group` can only bind to an Identifier or MemberExpression");
725+
}
726+
727+
/**
728+
* Can only bind to an Identifier or MemberExpression or a `{get, set}` pair
720729
* @param {null | number | NodeLike} node
721730
* @returns {never}
722731
*/
723732
export function bind_invalid_expression(node) {
724-
e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression");
733+
e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression or a `{get, set}` pair");
725734
}
726735

727736
/**
@@ -735,6 +744,16 @@ export function bind_invalid_name(node, name, explanation) {
735744
e(node, "bind_invalid_name", explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`);
736745
}
737746

747+
/**
748+
* `bind:%name%={get, set}` must not have surrounding parentheses
749+
* @param {null | number | NodeLike} node
750+
* @param {string} name
751+
* @returns {never}
752+
*/
753+
export function bind_invalid_parens(node, name) {
754+
e(node, "bind_invalid_parens", `\`bind:${name}={get, set}\` must not have surrounding parentheses`);
755+
}
756+
738757
/**
739758
* `bind:%name%` can only be used with %elements%
740759
* @param {null | number | NodeLike} node

packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js

Lines changed: 118 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -17,102 +17,6 @@ import { is_content_editable_binding, is_svg } from '../../../../utils.js';
1717
* @param {Context} context
1818
*/
1919
export function BindDirective(node, context) {
20-
validate_no_const_assignment(node, node.expression, context.state.scope, true);
21-
22-
const assignee = node.expression;
23-
const left = object(assignee);
24-
25-
if (left === null) {
26-
e.bind_invalid_expression(node);
27-
}
28-
29-
const binding = context.state.scope.get(left.name);
30-
31-
if (assignee.type === 'Identifier') {
32-
// reassignment
33-
if (
34-
node.name !== 'this' && // bind:this also works for regular variables
35-
(!binding ||
36-
(binding.kind !== 'state' &&
37-
binding.kind !== 'raw_state' &&
38-
binding.kind !== 'prop' &&
39-
binding.kind !== 'bindable_prop' &&
40-
binding.kind !== 'each' &&
41-
binding.kind !== 'store_sub' &&
42-
!binding.updated)) // TODO wut?
43-
) {
44-
e.bind_invalid_value(node.expression);
45-
}
46-
47-
if (context.state.analysis.runes && binding?.kind === 'each') {
48-
e.each_item_invalid_assignment(node);
49-
}
50-
51-
if (binding?.kind === 'snippet') {
52-
e.snippet_parameter_assignment(node);
53-
}
54-
}
55-
56-
if (node.name === 'group') {
57-
if (!binding) {
58-
throw new Error('Cannot find declaration for bind:group');
59-
}
60-
61-
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
62-
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
63-
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
64-
// entries as keys.
65-
const each_blocks = [];
66-
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
67-
let ids = expression_ids;
68-
69-
let i = context.path.length;
70-
while (i--) {
71-
const parent = context.path[i];
72-
73-
if (parent.type === 'EachBlock') {
74-
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
75-
76-
if (references.length > 0) {
77-
parent.metadata.contains_group_binding = true;
78-
79-
each_blocks.push(parent);
80-
ids = ids.filter((id) => !references.includes(id));
81-
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
82-
}
83-
}
84-
}
85-
86-
// The identifiers that make up the binding expression form they key for the binding group.
87-
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
88-
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
89-
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
90-
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
91-
let group_name;
92-
93-
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
94-
if (b.length !== bindings.length || key !== keypath) continue;
95-
for (let i = 0; i < bindings.length; i++) {
96-
if (bindings[i] !== b[i]) continue outer;
97-
}
98-
group_name = group;
99-
}
100-
101-
if (!group_name) {
102-
group_name = context.state.scope.root.unique('binding_group');
103-
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
104-
}
105-
106-
node.metadata = {
107-
binding_group_name: group_name,
108-
parent_each_blocks: each_blocks
109-
};
110-
}
111-
112-
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
113-
w.bind_invalid_each_rest(binding.node, binding.node.name);
114-
}
115-
11620
const parent = context.path.at(-1);
11721

11822
if (
@@ -218,5 +122,123 @@ export function BindDirective(node, context) {
218122
}
219123
}
220124

125+
// When dealing with bind getters/setters skip the specific binding validation
126+
// Group bindings aren't supported for getter/setters so we don't need to handle
127+
// the metadata
128+
if (node.expression.type === 'SequenceExpression') {
129+
if (node.name === 'group') {
130+
e.bind_group_invalid_expression(node);
131+
}
132+
133+
let i = /** @type {number} */ (node.expression.start);
134+
while (context.state.analysis.source[--i] !== '{') {
135+
if (context.state.analysis.source[i] === '(') {
136+
e.bind_invalid_parens(node, node.name);
137+
}
138+
}
139+
140+
if (node.expression.expressions.length !== 2) {
141+
e.bind_invalid_expression(node);
142+
}
143+
144+
return;
145+
}
146+
147+
validate_no_const_assignment(node, node.expression, context.state.scope, true);
148+
149+
const assignee = node.expression;
150+
const left = object(assignee);
151+
152+
if (left === null) {
153+
e.bind_invalid_expression(node);
154+
}
155+
156+
const binding = context.state.scope.get(left.name);
157+
158+
if (assignee.type === 'Identifier') {
159+
// reassignment
160+
if (
161+
node.name !== 'this' && // bind:this also works for regular variables
162+
(!binding ||
163+
(binding.kind !== 'state' &&
164+
binding.kind !== 'raw_state' &&
165+
binding.kind !== 'prop' &&
166+
binding.kind !== 'bindable_prop' &&
167+
binding.kind !== 'each' &&
168+
binding.kind !== 'store_sub' &&
169+
!binding.updated)) // TODO wut?
170+
) {
171+
e.bind_invalid_value(node.expression);
172+
}
173+
174+
if (context.state.analysis.runes && binding?.kind === 'each') {
175+
e.each_item_invalid_assignment(node);
176+
}
177+
178+
if (binding?.kind === 'snippet') {
179+
e.snippet_parameter_assignment(node);
180+
}
181+
}
182+
183+
if (node.name === 'group') {
184+
if (!binding) {
185+
throw new Error('Cannot find declaration for bind:group');
186+
}
187+
188+
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
189+
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
190+
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
191+
// entries as keys.
192+
const each_blocks = [];
193+
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
194+
let ids = expression_ids;
195+
196+
let i = context.path.length;
197+
while (i--) {
198+
const parent = context.path[i];
199+
200+
if (parent.type === 'EachBlock') {
201+
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
202+
203+
if (references.length > 0) {
204+
parent.metadata.contains_group_binding = true;
205+
206+
each_blocks.push(parent);
207+
ids = ids.filter((id) => !references.includes(id));
208+
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
209+
}
210+
}
211+
}
212+
213+
// The identifiers that make up the binding expression form they key for the binding group.
214+
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
215+
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
216+
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
217+
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
218+
let group_name;
219+
220+
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
221+
if (b.length !== bindings.length || key !== keypath) continue;
222+
for (let i = 0; i < bindings.length; i++) {
223+
if (bindings[i] !== b[i]) continue outer;
224+
}
225+
group_name = group;
226+
}
227+
228+
if (!group_name) {
229+
group_name = context.state.scope.root.unique('binding_group');
230+
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
231+
}
232+
233+
node.metadata = {
234+
binding_group_name: group_name,
235+
parent_each_blocks: each_blocks
236+
};
237+
}
238+
239+
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
240+
w.bind_invalid_each_rest(binding.node, binding.node.name);
241+
}
242+
221243
context.next();
222244
}

0 commit comments

Comments
 (0)