feat: add support for bind getters/setters

bind-get-set
Dominic Gannaway 1 day ago
parent ac9b7de058
commit 11e4ba017e

@ -20,6 +20,14 @@ export function BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression;
if (assignee.type === 'SequenceExpression') {
if (assignee.expressions.length !== 2) {
e.bind_invalid_expression(node);
}
return;
}
const left = object(assignee);
if (left === null) {

@ -36,18 +36,26 @@ export function BindDirective(node, context) {
);
}
const get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
/** @type {Expression | undefined} */
let set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
)
);
if (get === set) {
set = undefined;
let get, set;
if (expression.type === 'SequenceExpression') {
const [get_expression, set_expression] = expression.expressions;
get = /** @type {Expression} */ (context.visit(get_expression));
set = /** @type {Expression} */ (context.visit(set_expression));
} else {
get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
)
);
if (get === set) {
set = undefined;
}
}
/** @type {CallExpression} */

@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression;
if (bound.type === 'SequenceExpression') {
return;
}
while (bound.type === 'MemberExpression') {
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
}

@ -161,49 +161,63 @@ export function build_component(node, component_name, context, anchor = context.
push_prop(b.init(attribute.name, value));
}
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, attribute, expression);
}
if (attribute.name === 'this') {
bind_this = attribute.expression;
if (attribute.expression.type === 'SequenceExpression') {
const [get_expression, set_expression] = attribute.expression.expressions;
const get = /** @type {Expression} */ (context.visit(get_expression));
const set = /** @type {Expression} */ (context.visit(set_expression));
const get_id = b.id(context.state.scope.generate('bind_get'));
const set_id = b.id(context.state.scope.generate('bind_set'));
context.state.init.push(b.var(get_id, get));
context.state.init.push(b.var(set_id, set));
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
} else {
if (dev) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
);
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, attribute, expression);
}
const is_store_sub =
attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
if (is_store_sub) {
if (attribute.name === 'this') {
bind_this = attribute.expression;
} else {
if (dev) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
);
}
const is_store_sub =
attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
if (is_store_sub) {
push_prop(
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
);
} else {
push_prop(b.get(attribute.name, [b.return(expression)]));
}
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
);
} else {
push_prop(b.get(attribute.name, [b.return(expression)]));
}
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
);
}
}
}

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
@ -157,11 +157,19 @@ export function build_update_assignment(state, id, init, value, update) {
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} expression
* @param {Identifier | MemberExpression | SequenceExpression} expression
* @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
*/
export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') {
const [get_expression, set_expression] = expression.expressions;
const get = /** @type {Expression} */ (visit(get_expression));
const set = /** @type {Expression} */ (visit(set_expression));
return b.call('$.bind_this', value, get, set);
}
/** @type {Identifier[]} */
const ids = [];
@ -238,6 +246,9 @@ export function build_bind_this(expression, value, { state, visit }) {
* @param {MemberExpression} expression
*/
export function validate_binding(state, binding, expression) {
if (binding.expression.type === 'SequenceExpression') {
return;
}
// If we are referencing a $store.foo then we don't need to add validation
const left = object(binding.expression);
const left_binding = left && state.scope.get(left.name);

@ -81,22 +81,36 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
);
push_prop(
b.set(attribute.name, [
b.stmt(
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
);
if (attribute.expression.type === 'SequenceExpression') {
const [get_expression, set_expression] = attribute.expression.expressions;
const get = /** @type {Expression} */ (context.visit(get_expression));
const set = /** @type {Expression} */ (context.visit(set_expression));
const get_id = b.id(context.state.scope.generate('bind_get'));
const set_id = b.id(context.state.scope.generate('bind_set'));
context.state.init.push(b.var(get_id, get));
context.state.init.push(b.var(set_id, set));
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
} else {
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
push_prop(
b.get(attribute.name, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
);
push_prop(
b.set(attribute.name, [
b.stmt(
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
);
}
}
}

@ -110,11 +110,16 @@ export function build_element_attributes(node, context) {
if (binding?.omit_in_ssr) continue;
if (is_content_editable_binding(attribute.name)) {
content = /** @type {Expression} */ (context.visit(attribute.expression));
content =
attribute.expression.type === 'SequenceExpression'
? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(attribute.expression));
} else if (attribute.name === 'value' && node.name === 'textarea') {
content = b.call(
'$.escape',
/** @type {Expression} */ (context.visit(attribute.expression))
attribute.expression.type === 'SequenceExpression'
? b.call(/** @type {Expression} */ (context.visit(attribute.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(attribute.expression))
);
} else if (attribute.name === 'group') {
const value_attribute = /** @type {AST.Attribute | undefined} */ (
@ -129,6 +134,11 @@ export function build_element_attributes(node, context) {
is_text_attribute(attr) &&
attr.value[0].data === 'checkbox'
);
const attribute_expression =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: attribute.expression;
attributes.push(
create_attribute('checked', -1, -1, [
{
@ -138,12 +148,12 @@ export function build_element_attributes(node, context) {
parent: attribute,
expression: is_checkbox
? b.call(
b.member(attribute.expression, 'includes'),
b.member(attribute_expression, 'includes'),
build_attribute_value(value_attribute.value, context)
)
: b.binary(
'===',
attribute.expression,
attribute_expression,
build_attribute_value(value_attribute.value, context)
),
metadata: {
@ -153,6 +163,11 @@ export function build_element_attributes(node, context) {
])
);
} else {
const attribute_expression =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: attribute.expression;
attributes.push(
create_attribute(attribute.name, -1, -1, [
{
@ -160,7 +175,7 @@ export function build_element_attributes(node, context) {
start: -1,
end: -1,
parent: attribute,
expression: attribute.expression,
expression: attribute_expression,
metadata: {
expression: create_expression_metadata()
}

@ -6,7 +6,8 @@ import type {
Identifier,
MemberExpression,
ObjectExpression,
Pattern
Pattern,
SequenceExpression
} from 'estree';
interface BaseNode {
@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode {
/** The 'x' in `bind:x` */
name: string;
/** The y in `bind:x={y}` */
expression: Identifier | MemberExpression;
expression: Identifier | MemberExpression | SequenceExpression;
}
export interface LegacyBody extends BaseElement {

@ -14,7 +14,8 @@ import type {
Pattern,
Program,
ChainExpression,
SimpleCallExpression
SimpleCallExpression,
SequenceExpression
} from 'estree';
import type { Scope } from '../phases/scope';
@ -185,7 +186,7 @@ export namespace AST {
/** The 'x' in `bind:x` */
name: string;
/** The y in `bind:x={y}` */
expression: Identifier | MemberExpression;
expression: Identifier | MemberExpression | SequenceExpression;
/** @internal */
metadata: {
binding_group_name: Identifier;

@ -606,7 +606,7 @@ declare module 'svelte/animate' {
}
declare module 'svelte/compiler' {
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression } from 'estree';
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { SourceMap } from 'magic-string';
import type { Location } from 'locate-character';
/**
@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' {
/** The 'x' in `bind:x` */
name: string;
/** The y in `bind:x={y}` */
expression: Identifier | MemberExpression;
expression: Identifier | MemberExpression | SequenceExpression;
}
/** A `class:` directive */

Loading…
Cancel
Save