Skip to content

Commit be44125

Browse files
committed
feat: support bind:value={get, set}
Companion to sveltejs/svelte#14307
1 parent 695c660 commit be44125

File tree

5 files changed

+77
-41
lines changed

5 files changed

+77
-41
lines changed
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
<svelte:options runes />
1+
<input bind:value={get, set} />
2+
<input bind:value={() => v, new_v => v = new_v} />
23

3-
<script>
4-
let name = "world"
5-
let name2 = "world"
6-
export { name as name3, name2 as name4 };
7-
</script>
4+
<div bind:clientWidth={null, set} />
5+
<div bind:contentRect={null, set} />
6+
7+
<Input bind:value={get, set} />
8+
<Input bind:value={() => v, new_v => v = new_v} />

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { BaseDirective, BaseNode } from '../../interfaces';
99
import { Element } from './Element';
1010
import { InlineComponent } from './InlineComponent';
1111
import { surroundWithIgnoreComments } from '../../utils/ignore';
12+
import { SequenceExpression } from 'estree';
1213

1314
/**
1415
* List of binding names that are transformed to sth like `binding = variable`.
@@ -58,47 +59,54 @@ export function handleBinding(
5859
preserveBind: boolean,
5960
isSvelte5Plus: boolean
6061
): void {
61-
// bind group on input
62-
if (element instanceof Element && attr.name == 'group' && parent.name == 'input') {
63-
// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
64-
appendOneWayBinding(attr, ' = __sveltets_2_any(null)', element);
65-
return;
66-
}
62+
const isGetSetBinding = attr.expression.type === 'SequenceExpression';
6763

68-
// bind this
69-
if (attr.name === 'this' && supportsBindThis.includes(parent.type)) {
70-
// bind:this is effectively only works bottom up - the variable is updated by the element, not
71-
// the other way round. So we check if the instance is assignable to the variable.
72-
// Note: If the component unmounts (it's inside an if block, or svelte:component this={null},
73-
// the value becomes null, but we don't add it to the clause because it would introduce
74-
// worse DX for the 99% use case, and because null !== undefined which others might use to type the declaration.
75-
appendOneWayBinding(attr, ` = ${element.name}`, element);
76-
return;
77-
}
64+
if (!isGetSetBinding) {
65+
// bind group on input
66+
if (element instanceof Element && attr.name == 'group' && parent.name == 'input') {
67+
// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
68+
appendOneWayBinding(attr, ' = __sveltets_2_any(null)', element);
69+
return;
70+
}
7871

79-
// one way binding
80-
if (oneWayBindingAttributes.has(attr.name) && element instanceof Element) {
81-
appendOneWayBinding(attr, `= ${element.name}.${attr.name}`, element);
82-
return;
83-
}
72+
// bind this
73+
if (attr.name === 'this' && supportsBindThis.includes(parent.type)) {
74+
// bind:this is effectively only works bottom up - the variable is updated by the element, not
75+
// the other way round. So we check if the instance is assignable to the variable.
76+
// Note: If the component unmounts (it's inside an if block, or svelte:component this={null},
77+
// the value becomes null, but we don't add it to the clause because it would introduce
78+
// worse DX for the 99% use case, and because null !== undefined which others might use to type the declaration.
79+
appendOneWayBinding(attr, ` = ${element.name}`, element);
80+
return;
81+
}
8482

85-
// one way binding whose property is not on the element
86-
if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) {
83+
// one way binding
84+
if (oneWayBindingAttributes.has(attr.name) && element instanceof Element) {
85+
appendOneWayBinding(attr, `= ${element.name}.${attr.name}`, element);
86+
return;
87+
}
88+
89+
// one way binding whose property is not on the element
90+
if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) {
91+
element.appendToStartEnd([
92+
[attr.expression.start, getEnd(attr.expression)],
93+
`= ${surroundWithIgnoreComments(
94+
`null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}`
95+
)};`
96+
]);
97+
return;
98+
}
99+
100+
// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
101+
const expressionStr = str.original.substring(
102+
attr.expression.start,
103+
getEnd(attr.expression)
104+
);
87105
element.appendToStartEnd([
88-
[attr.expression.start, getEnd(attr.expression)],
89-
`= ${surroundWithIgnoreComments(
90-
`null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}`
91-
)};`
106+
surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`)
92107
]);
93-
return;
94108
}
95109

96-
// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
97-
const expressionStr = str.original.substring(attr.expression.start, getEnd(attr.expression));
98-
element.appendToStartEnd([
99-
surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`)
100-
]);
101-
102110
// other bindings which are transformed to normal attributes/props
103111
const isShorthand = attr.expression.start === attr.start + 'bind:'.length;
104112
const name: TransformationArray =
@@ -122,11 +130,20 @@ export function handleBinding(
122130
]
123131
];
124132

133+
const [get, set] = isGetSetBinding ? (attr.expression as SequenceExpression).expressions : [];
125134
const value: TransformationArray | undefined = isShorthand
126135
? preserveBind && element instanceof Element
127136
? [rangeWithTrailingPropertyAccess(str.original, attr.expression)]
128137
: undefined
129-
: [rangeWithTrailingPropertyAccess(str.original, attr.expression)];
138+
: isGetSetBinding
139+
? [
140+
'__sveltets_2_get_set_binding(',
141+
[get.start, get.end],
142+
',',
143+
rangeWithTrailingPropertyAccess(str.original, set),
144+
')'
145+
]
146+
: [rangeWithTrailingPropertyAccess(str.original, attr.expression)];
130147

131148
if (isSvelte5Plus && element instanceof InlineComponent) {
132149
// To check if property is actually bindable

packages/svelte2tsx/svelte-shims-v4.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ type __sveltets_2_PropsWithChildren<Props, Slots> = Props &
263263
: {});
264264
declare function __sveltets_2_runes_constructor<Props extends {}>(render: {props: Props }): import("svelte").ComponentConstructorOptions<Props>;
265265

266+
declare function __sveltets_2_get_set_binding<T>(get: (() => T) | null | undefined, set: (t: T) => void): T;
267+
266268
declare function __sveltets_$$bindings<Bindings extends string[]>(...bindings: Bindings): Bindings[number];
267269

268270
declare function __sveltets_2_fn_component<
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{ svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(get,set),});}
2+
{ svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(() => v,new_v => v = new_v),});}
3+
4+
{ svelteHTML.createElement("div", { "bind:clientWidth":__sveltets_2_get_set_binding(null,set),});}
5+
{ svelteHTML.createElement("div", { "bind:contentRect":__sveltets_2_get_set_binding(null,set),});}
6+
7+
{ const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:__sveltets_2_get_set_binding(get,set),}});$$_tupnI0.$$bindings = 'value';}
8+
{ const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:__sveltets_2_get_set_binding(() => v,new_v => v = new_v),}});$$_tupnI0.$$bindings = 'value';}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<input bind:value={get, set} />
2+
<input bind:value={() => v, new_v => v = new_v} />
3+
4+
<div bind:clientWidth={null, set} />
5+
<div bind:contentRect={null, set} />
6+
7+
<Input bind:value={get, set} />
8+
<Input bind:value={() => v, new_v => v = new_v} />

0 commit comments

Comments
 (0)