From be4412509e1fed62b39665bd340079848ecb6c41 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 10:22:27 +0100 Subject: [PATCH] feat: support `bind:value={get, set}` Companion to https://github.com/sveltejs/svelte/pull/14307 --- packages/svelte2tsx/repl/index.svelte | 13 +-- .../src/htmlxtojsx_v2/nodes/Binding.ts | 87 +++++++++++-------- packages/svelte2tsx/svelte-shims-v4.d.ts | 2 + .../samples/.binding-get-set.v5/expectedv2.js | 8 ++ .../samples/.binding-get-set.v5/input.svelte | 8 ++ 5 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte diff --git a/packages/svelte2tsx/repl/index.svelte b/packages/svelte2tsx/repl/index.svelte index 4b1efc883..9334f7ac0 100644 --- a/packages/svelte2tsx/repl/index.svelte +++ b/packages/svelte2tsx/repl/index.svelte @@ -1,7 +1,8 @@ - + + v, new_v => v = new_v} /> - +
+
+ + + v, new_v => v = new_v} /> diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts index 5bb17cca7..73baf0352 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts @@ -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`. @@ -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 = @@ -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 diff --git a/packages/svelte2tsx/svelte-shims-v4.d.ts b/packages/svelte2tsx/svelte-shims-v4.d.ts index 2d36a539d..fcaf92f65 100644 --- a/packages/svelte2tsx/svelte-shims-v4.d.ts +++ b/packages/svelte2tsx/svelte-shims-v4.d.ts @@ -263,6 +263,8 @@ type __sveltets_2_PropsWithChildren = Props & : {}); declare function __sveltets_2_runes_constructor(render: {props: Props }): import("svelte").ComponentConstructorOptions; +declare function __sveltets_2_get_set_binding(get: (() => T) | null | undefined, set: (t: T) => void): T; + declare function __sveltets_$$bindings(...bindings: Bindings): Bindings[number]; declare function __sveltets_2_fn_component< diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js new file mode 100644 index 000000000..370353fb4 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js @@ -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';} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte new file mode 100644 index 000000000..9334f7ac0 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte @@ -0,0 +1,8 @@ + + v, new_v => v = new_v} /> + +
+
+ + + v, new_v => v = new_v} />