diff --git a/.changeset/tall-donkeys-sit.md b/.changeset/tall-donkeys-sit.md new file mode 100644 index 000000000000..5eb42e4575a3 --- /dev/null +++ b/.changeset/tall-donkeys-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support for spreading function bindings diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md index de57815687dc..1772d5b600a4 100644 --- a/documentation/docs/03-template-syntax/12-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -40,6 +40,22 @@ In the case of readonly bindings like [dimension bindings](#Dimensions), the `ge > [!NOTE] > Function bindings are available in Svelte 5.9.0 and newer. +If you already have a `[get, set]` tuple or an object with `get` and/or `set` functions, you can use the spread syntax to bind them directly, instead of destructuring them beforehand. +This is especially handy when using helpers that return getter/setter pairs. + +```svelte + + + +``` + ## `` A `bind:value` directive on an `` element binds the input's `value` property: diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 957a9f67c7b0..640b0c125cee 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -93,7 +93,7 @@ Cannot `bind:group` to a snippet parameter ### bind_invalid_expression ``` -Can only bind to an Identifier or MemberExpression or a `{get, set}` pair +Can only bind to an Identifier, a MemberExpression, a SpreadElement, or a `{get, set}` pair ``` ### bind_invalid_name diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index de34b3f5da7c..338aa9b0908c 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -56,6 +56,12 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` ``` +### invalid_spread_bindings + +``` +`%name%` must be a function or `undefined` +``` + ### lifecycle_outside_component ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 0569f63ad30d..54ecd948069b 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -60,7 +60,7 @@ ## bind_invalid_expression -> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair +> Can only bind to an Identifier, a MemberExpression, a SpreadElement, or a `{get, set}` pair ## bind_invalid_name diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index f9160671d3f6..fde6169c0b50 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -48,6 +48,10 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P > A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` +## invalid_spread_bindings + +> `%name%` must be a function or `undefined` + ## lifecycle_outside_component > `%name%(...)` can only be used during component initialisation diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index e763a6e0733a..bb1154466432 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -830,12 +830,12 @@ export function bind_group_invalid_snippet_parameter(node) { } /** - * Can only bind to an Identifier or MemberExpression or a `{get, set}` pair + * Can only bind to an Identifier, a MemberExpression, a SpreadElement, or a `{get, set}` pair * @param {null | number | NodeLike} node * @returns {never} */ export function bind_invalid_expression(node) { - e(node, 'bind_invalid_expression', `Can only bind to an Identifier or MemberExpression or a \`{get, set}\` pair\nhttps://svelte.dev/e/bind_invalid_expression`); + e(node, 'bind_invalid_expression', `Can only bind to an Identifier, a MemberExpression, a SpreadElement, or a \`{get, set}\` pair\nhttps://svelte.dev/e/bind_invalid_expression`); } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index ed1b047d5556..810513e4f979 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -649,8 +649,7 @@ function read_attribute(parser) { } } - /** @type {AST.Directive} */ - const directive = { + const directive = /** @type {AST.Directive} */ ({ start, end, type, @@ -659,7 +658,13 @@ function read_attribute(parser) { metadata: { expression: create_expression_metadata() } - }; + }); + if (first_value?.metadata.expression.has_spread) { + if (directive.type !== 'BindDirective') { + e.directive_invalid_value(first_value.start); + } + directive.metadata.spread_binding = true; + } // @ts-expect-error we do this separately from the declaration to avoid upsetting typescript directive.modifiers = modifiers; @@ -812,6 +817,12 @@ function read_sequence(parser, done, location) { flush(parser.index - 1); parser.allow_whitespace(); + + const has_spread = parser.eat('...'); + if (has_spread) { + parser.allow_whitespace(); + } + const expression = read_expression(parser); parser.allow_whitespace(); parser.eat('}', true); @@ -827,6 +838,8 @@ function read_sequence(parser, done, location) { } }; + chunk.metadata.expression.has_spread = has_spread; + chunks.push(chunk); current_chunk = { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 9f02e7fa5a02..f0efc56f96f3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -158,6 +158,16 @@ export function BindDirective(node, context) { return; } + if (node.metadata.spread_binding) { + if (node.name === 'group') { + e.bind_group_invalid_expression(node); + } + + mark_subtree_dynamic(context.path); + + return; + } + validate_assignment(node, node.expression, context); const assignee = node.expression; @@ -242,7 +252,8 @@ export function BindDirective(node, context) { node.metadata = { binding_group_name: group_name, - parent_each_blocks: each_blocks + parent_each_blocks: each_blocks, + spread_binding: node.metadata.spread_binding }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 506fd4aafd82..a55ba2a37a9e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -7,6 +7,7 @@ import * as b from '#compiler/builders'; import { binding_properties } from '../../../bindings.js'; import { build_attribute_value } from './shared/element.js'; import { build_bind_this, validate_binding } from './shared/utils.js'; +import { init_spread_bindings } from '../../shared/spread_bindings.js'; /** * @param {AST.BindDirective} node @@ -20,7 +21,11 @@ export function BindDirective(node, context) { let get, set; - if (expression.type === 'SequenceExpression') { + if (node.metadata.spread_binding) { + const { get: getter, set: setter } = init_spread_bindings(node.expression, context); + get = getter; + set = setter; + } else if (expression.type === 'SequenceExpression') { [get, set] = expression.expressions; } else { if ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 4296aa959e87..c305a0cb6a6f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -414,7 +414,7 @@ function setup_select_synchronization(value_binding, context) { let bound = value_binding.expression; - if (bound.type === 'SequenceExpression') { + if (bound.type === 'SequenceExpression' || value_binding.metadata.spread_binding) { return; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 7feeebdbbc4e..74e5f1c98968 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement, SpreadElement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; @@ -8,6 +8,7 @@ import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '.. import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -48,7 +49,7 @@ export function build_component(node, component_name, context) { /** @type {Property[]} */ const custom_css_props = []; - /** @type {Identifier | MemberExpression | SequenceExpression | null} */ + /** @type {Identifier | MemberExpression | SequenceExpression | SpreadElement | null} */ let bind_this = null; /** @type {ExpressionStatement[]} */ @@ -202,8 +203,10 @@ export function build_component(node, component_name, context) { dev && attribute.name !== 'this' && !is_ignored(node, 'ownership_invalid_binding') && - // bind:x={() => x.y, y => x.y = y} will be handled by the assignment expression binding validation - attribute.expression.type !== 'SequenceExpression' + // bind:x={() => x.y, y => x.y = y} and bind:x={...[() => x.y, y => x.y = y]} + // will be handled by the assignment expression binding validation + attribute.expression.type !== 'SequenceExpression' && + !attribute.metadata.spread_binding ) { const left = object(attribute.expression); const binding = left && context.state.scope.get(left.name); @@ -223,7 +226,19 @@ export function build_component(node, component_name, context) { } } - if (expression.type === 'SequenceExpression') { + if (attribute.metadata.spread_binding) { + const { get, set } = init_spread_bindings(attribute.expression, context); + + if (attribute.name === 'this') { + bind_this = { + type: 'SpreadElement', + argument: attribute.expression + }; + } else { + push_prop(b.get(attribute.name, [b.return(b.call(get))]), true); + push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))]), true); + } + } else if (expression.type === 'SequenceExpression') { if (attribute.name === 'this') { bind_this = attribute.expression; } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 014547cf2de3..7a443693d337 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, SpreadElement, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { build_getter } from '../../utils.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; /** * A utility for extracting complex expressions (such as call expressions) @@ -204,11 +205,17 @@ export function parse_directive_name(name) { /** * Serializes `bind:this` for components and elements. - * @param {Identifier | MemberExpression | SequenceExpression} expression + * @param {Identifier | MemberExpression | SequenceExpression | SpreadElement} expression * @param {Expression} value * @param {import('zimmerframe').Context} context */ -export function build_bind_this(expression, value, { state, visit }) { +export function build_bind_this(expression, value, context) { + const { state, visit } = context; + if (expression.type === 'SpreadElement') { + const { get, set } = init_spread_bindings(expression.argument, context); + return b.call('$.bind_this', value, set, get); + } + if (expression.type === 'SequenceExpression') { const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; return b.call('$.bind_this', value, set, get); @@ -290,7 +297,7 @@ export function build_bind_this(expression, value, { state, visit }) { * @param {MemberExpression} expression */ export function validate_binding(state, binding, expression) { - if (binding.expression.type === 'SequenceExpression') { + if (binding.expression.type === 'SequenceExpression' || binding.metadata.spread_binding) { return; } // If we are referencing a $store.foo then we don't need to add validation diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 9bccf9e05e05..050e5077bcb8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -5,6 +5,7 @@ import { empty_comment, build_attribute_value } from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -93,7 +94,12 @@ 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') { - if (attribute.expression.type === 'SequenceExpression') { + if (attribute.metadata.spread_binding) { + const { get, set } = init_spread_bindings(attribute.expression, context); + + push_prop(b.get(attribute.name, [b.return(b.call(get))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set, b.id('$$value')))])); + } else if (attribute.expression.type === 'SequenceExpression') { const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) .expressions; const get_id = b.id(context.state.scope.generate('bind_get')); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 7207564ef983..0bb4ca87a648 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -21,6 +21,7 @@ import { is_load_error_element } from '../../../../../../utils.js'; import { escape_html } from '../../../../../../escaping.js'; +import { init_spread_bindings } from '../../../shared/spread_bindings.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; @@ -118,7 +119,10 @@ export function build_element_attributes(node, context) { let expression = /** @type {Expression} */ (context.visit(attribute.expression)); - if (expression.type === 'SequenceExpression') { + if (attribute.metadata.spread_binding) { + const { get } = init_spread_bindings(attribute.expression, context); + expression = b.call(get); + } else if (expression.type === 'SequenceExpression') { expression = b.call(expression.expressions[0]); } @@ -126,7 +130,11 @@ export function build_element_attributes(node, context) { content = expression; } else if (attribute.name === 'value' && node.name === 'textarea') { content = b.call('$.escape', expression); - } else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') { + } else if ( + attribute.name === 'group' && + attribute.expression.type !== 'SequenceExpression' && + !attribute.metadata.spread_binding + ) { const value_attribute = /** @type {AST.Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') ); diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js b/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js new file mode 100644 index 000000000000..6414be66b5e0 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/spread_bindings.js @@ -0,0 +1,30 @@ +/** @import { Expression } from 'estree' */ +/** @import { ComponentContext as ClientContext } from '../client/types.js' */ +/** @import { ComponentContext as ServerContext } from '../server/types.js' */ +import * as b from '#compiler/builders'; +import { dev, source } from '../../../state.js'; + +/** + * Initializes spread bindings for a SpreadElement in a bind directive. + * @param {Expression} spread_expression + * @param {ClientContext | ServerContext} context + * @returns {{ get: Expression, set: Expression }} + */ +export function init_spread_bindings(spread_expression, { state, visit }) { + const expression = /** @type {Expression} */ (visit(spread_expression)); + const expression_text = dev + ? b.literal(source.slice(spread_expression.start, spread_expression.end)) + : undefined; + + const id = state.scope.generate('$$spread_binding'); + const get = b.id(id + '_get'); + const set = b.id(id + '_set'); + state.init.push( + b.const( + b.array_pattern([get, set]), + b.call('$.validate_spread_bindings', expression, expression_text) + ) + ); + + return { get, set }; +} diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 4874554ff0fb..6e29c23be8ee 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -67,7 +67,8 @@ export function create_expression_metadata() { has_call: false, has_member_expression: false, has_assignment: false, - has_await: false + has_await: false, + has_spread: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 6211e69bd3e1..7d8c51db0f6d 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -304,6 +304,8 @@ export interface ExpressionMetadata { has_member_expression: boolean; /** True if the expression includes an assignment or an update */ has_assignment: boolean; + /** True if the expression includes a spread element */ + has_spread: boolean; } export interface StateField { diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 389fc923327a..259320ca400a 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -7,7 +7,8 @@ import type { MemberExpression, ObjectExpression, Pattern, - SequenceExpression + SequenceExpression, + SpreadElement } from 'estree'; interface BaseNode { @@ -50,7 +51,7 @@ export interface LegacyBinding extends BaseNode { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression | SequenceExpression; + expression: Identifier | MemberExpression | SequenceExpression | SpreadElement; } export interface LegacyBody extends BaseElement { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 060df2dcb2a4..6c8701d6b97e 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -216,6 +216,7 @@ export namespace AST { metadata: { binding_group_name: Identifier; parent_each_blocks: EachBlock[]; + spread_binding: boolean; }; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c094c9e04449..34781d10391b 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -172,7 +172,8 @@ export { validate_dynamic_element_tag, validate_store, validate_void_dynamic_element, - prevent_snippet_stringification + prevent_snippet_stringification, + validate_spread_bindings } from '../shared/validate.js'; export { strict_equals, equals } from './dev/equality.js'; export { log_if_contains_state } from './dev/console-log.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fcda..37aadabc321a 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -517,7 +517,8 @@ export { invalid_default_snippet, validate_dynamic_element_tag, validate_void_dynamic_element, - prevent_snippet_stringification + prevent_snippet_stringification, + validate_spread_bindings } from '../shared/validate.js'; export { escape_html as escape }; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 66685cb00b75..a2056dd071c7 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -50,6 +50,23 @@ export function invalid_snippet_arguments() { } } +/** + * `%name%` must be a function or `undefined` + * @param {string} name + * @returns {never} + */ +export function invalid_spread_bindings(name) { + if (DEV) { + const error = new Error(`invalid_spread_bindings\n\`${name}\` must be a function or \`undefined\`\nhttps://svelte.dev/e/invalid_spread_bindings`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/invalid_spread_bindings`); + } +} + /** * `%name%(...)` can only be used during component initialisation * @param {string} name diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 48e76f09589d..720baa03cb7d 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,6 +1,7 @@ import { is_void } from '../../utils.js'; import * as w from './warnings.js'; import * as e from './errors.js'; +import { noop } from './utils.js'; export { invalid_default_snippet } from './errors.js'; @@ -45,3 +46,23 @@ export function prevent_snippet_stringification(fn) { }; return fn; } + +/** + * @param {any} spread_object + * @param {string} name + * @return {[() => unknown, (value: unknown) => void]} + */ +export function validate_spread_bindings(spread_object, name) { + const is_array = Array.isArray(spread_object); + const getter = is_array ? spread_object[0] : spread_object.get; + const setter = is_array ? spread_object[1] : spread_object.set; + + if (typeof getter !== 'function' && getter != null) { + e.invalid_spread_bindings(name + (is_array ? '[0]' : '.get')); + } + if (typeof setter !== 'function' && setter != null) { + e.invalid_spread_bindings(name + (is_array ? '[1]' : '.set')); + } + + return [getter ?? noop, setter ?? noop]; +} diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte new file mode 100644 index 000000000000..5c8fd19fa46f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/Child.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js new file mode 100644 index 000000000000..dd5c387405e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/_config.js @@ -0,0 +1,26 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { assert_ok } from '../../../suite'; + +export default test({ + async test({ assert, target, logs }) { + const [input, checkbox] = target.querySelectorAll('input'); + + input.value = '2'; + input.dispatchEvent(new window.Event('input')); + + flushSync(); + + assert.htmlEqual( + target.innerHTML, + `
` + ); + + assert.deepEqual(logs, ['b', '2', 'a', '2']); + + flushSync(() => { + checkbox.click(); + }); + assert.deepEqual(logs, ['b', '2', 'a', '2', 'check', false]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte new file mode 100644 index 000000000000..7dc2aaf317e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-component/main.svelte @@ -0,0 +1,30 @@ + + + + + + +
+ +
diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/_config.js new file mode 100644 index 000000000000..57cce72d3006 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const checkboxes = target.querySelectorAll('input'); + + flushSync(); + + assert.htmlEqual(target.innerHTML, ``.repeat(checkboxes.length)); + + checkboxes.forEach((checkbox) => checkbox.click()); + + assert.deepEqual(logs, repeatArray(checkboxes.length, ['change', true])); + } +}); + +/** @template T */ +function repeatArray(/** @type {number} */ times, /** @type {T[]} */ array) { + return /** @type {T[]} */ Array.from({ length: times }, () => array).flat(); +} diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/main.svelte new file mode 100644 index 000000000000..e1c37443f673 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-empty/main.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-error/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread-error/_config.js new file mode 100644 index 000000000000..65c6deeda0b7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-error/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + expect_unhandled_rejections: true, + compileOptions: { + dev: true + }, + error: 'invalid_spread_bindings' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread-error/main.svelte new file mode 100644 index 000000000000..8dc8f03644f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread-error/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js new file mode 100644 index 000000000000..2161bdea2d7e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const checkboxes = target.querySelectorAll('input'); + + flushSync(); + + assert.htmlEqual(target.innerHTML, ``.repeat(checkboxes.length)); + + checkboxes.forEach((checkbox) => checkbox.click()); + + assert.deepEqual(logs, [ + 'getArrayBindings', + 'getObjectBindings', + ...repeatArray(checkboxes.length, ['check', false]) + ]); + } +}); + +/** @template T */ +function repeatArray(/** @type {number} */ times, /** @type {T[]} */ array) { + return /** @type {T[]} */ Array.from({ length: times }, () => array).flat(); +} diff --git a/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte new file mode 100644 index 000000000000..7f11eb81f6db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-spread/main.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + + [get, set])()} /> + +