Skip to content

Commit

Permalink
feat: support bind:value={get, set}
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed Nov 22, 2024
1 parent 695c660 commit be44125
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 41 deletions.
13 changes: 7 additions & 6 deletions packages/svelte2tsx/repl/index.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<svelte:options runes />
<input bind:value={get, set} />
<input bind:value={() => v, new_v => v = new_v} />

<script>
let name = "world"
let name2 = "world"
export { name as name3, name2 as name4 };
</script>
<div bind:clientWidth={null, set} />
<div bind:contentRect={null, set} />

<Input bind:value={get, set} />
<Input bind:value={() => v, new_v => v = new_v} />
87 changes: 52 additions & 35 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BaseDirective, BaseNode } from '../../interfaces';
import { Element } from './Element';
import { InlineComponent } from './InlineComponent';
import { surroundWithIgnoreComments } from '../../utils/ignore';
import { SequenceExpression } from 'estree';

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

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

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

// one way binding whose property is not on the element
if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) {
// one way binding
if (oneWayBindingAttributes.has(attr.name) && element instanceof Element) {
appendOneWayBinding(attr, `= ${element.name}.${attr.name}`, element);
return;
}

// one way binding whose property is not on the element
if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) {
element.appendToStartEnd([
[attr.expression.start, getEnd(attr.expression)],
`= ${surroundWithIgnoreComments(
`null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}`
)};`
]);
return;
}

// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
const expressionStr = str.original.substring(
attr.expression.start,
getEnd(attr.expression)
);
element.appendToStartEnd([
[attr.expression.start, getEnd(attr.expression)],
`= ${surroundWithIgnoreComments(
`null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}`
)};`
surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`)
]);
return;
}

// add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else)
const expressionStr = str.original.substring(attr.expression.start, getEnd(attr.expression));
element.appendToStartEnd([
surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`)
]);

// other bindings which are transformed to normal attributes/props
const isShorthand = attr.expression.start === attr.start + 'bind:'.length;
const name: TransformationArray =
Expand All @@ -122,11 +130,20 @@ export function handleBinding(
]
];

const [get, set] = isGetSetBinding ? (attr.expression as SequenceExpression).expressions : [];
const value: TransformationArray | undefined = isShorthand
? preserveBind && element instanceof Element
? [rangeWithTrailingPropertyAccess(str.original, attr.expression)]
: undefined
: [rangeWithTrailingPropertyAccess(str.original, attr.expression)];
: isGetSetBinding
? [
'__sveltets_2_get_set_binding(',
[get.start, get.end],
',',
rangeWithTrailingPropertyAccess(str.original, set),
')'
]
: [rangeWithTrailingPropertyAccess(str.original, attr.expression)];

if (isSvelte5Plus && element instanceof InlineComponent) {
// To check if property is actually bindable
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte2tsx/svelte-shims-v4.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ type __sveltets_2_PropsWithChildren<Props, Slots> = Props &
: {});
declare function __sveltets_2_runes_constructor<Props extends {}>(render: {props: Props }): import("svelte").ComponentConstructorOptions<Props>;

declare function __sveltets_2_get_set_binding<T>(get: (() => T) | null | undefined, set: (t: T) => void): T;

declare function __sveltets_$$bindings<Bindings extends string[]>(...bindings: Bindings): Bindings[number];

declare function __sveltets_2_fn_component<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{ svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(get,set),});}
{ svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(() => v,new_v => v = new_v),});}

{ svelteHTML.createElement("div", { "bind:clientWidth":__sveltets_2_get_set_binding(null,set),});}
{ svelteHTML.createElement("div", { "bind:contentRect":__sveltets_2_get_set_binding(null,set),});}

{ 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';}
{ 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';}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<input bind:value={get, set} />
<input bind:value={() => v, new_v => v = new_v} />

<div bind:clientWidth={null, set} />
<div bind:contentRect={null, set} />

<Input bind:value={get, set} />
<Input bind:value={() => v, new_v => v = new_v} />

0 comments on commit be44125

Please sign in to comment.