diff --git a/.changeset/red-coats-grin.md b/.changeset/red-coats-grin.md new file mode 100644 index 0000000000..0ff1bc84a9 --- /dev/null +++ b/.changeset/red-coats-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent spread attribute from overriding class directive 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 d4624c9d3a..5484c37cb7 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 @@ -219,7 +219,8 @@ export function RegularElement(node, context) { node_id, attributes_id, (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true, - node.name.includes('-') && b.true + node.name.includes('-') && b.true, + context.state ); // If value binding exists, that one takes care of calling $.init_select diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index 85a2b04d91..8bae80be37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -102,7 +102,8 @@ export function SvelteElement(node, context) { element_id, attributes_id, b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')), - b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')) + b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')), + context.state ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index d2bf3d53ce..1d0705b88d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,6 +1,6 @@ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ -/** @import { ComponentContext } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js'; @@ -16,6 +16,7 @@ import { build_template_literal, build_update } from './utils.js'; * @param {Identifier} attributes_id * @param {false | Expression} preserve_attribute_case * @param {false | Expression} is_custom_element + * @param {ComponentClientTransformState} state */ export function build_set_attributes( attributes, @@ -24,9 +25,9 @@ export function build_set_attributes( element_id, attributes_id, preserve_attribute_case, - is_custom_element + is_custom_element, + state ) { - let needs_isolation = false; let has_state = false; /** @type {ObjectExpression['properties']} */ @@ -50,12 +51,17 @@ export function build_set_attributes( has_state ||= attribute.metadata.expression.has_state; } else { - values.push(b.spread(/** @type {Expression} */ (context.visit(attribute)))); - // objects could contain reactive getters -> play it safe and always assume spread attributes are reactive has_state = true; - needs_isolation ||= attribute.metadata.expression.has_call; + let value = /** @type {Expression} */ (context.visit(attribute)); + + if (attribute.metadata.expression.has_call) { + const id = b.id(state.scope.generate('spread_with_call')); + state.init.push(b.const(id, create_derived(state, b.thunk(value)))); + value = b.call('$.get', id); + } + values.push(b.spread(value)); } } @@ -72,14 +78,7 @@ export function build_set_attributes( if (has_state) { context.state.init.push(b.let(attributes_id)); - const update = b.stmt(b.assignment('=', attributes_id, call)); - - if (needs_isolation) { - context.state.init.push(build_update(update)); - return false; - } - context.state.update.push(update); return true; } diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/_config.js new file mode 100644 index 0000000000..67d57915e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test, ok } from '../../test'; + +export default test({ + test({ target, logs, assert }) { + const input = target.querySelector('input'); + + ok(input); + + assert.deepEqual(logs, ['get_rest']); + + assert.ok(input.classList.contains('dark')); + assert.equal(input.dataset.rest, 'true'); + + flushSync(() => { + input.focus(); + }); + + assert.ok(input.classList.contains('dark')); + assert.ok(input.classList.contains('focused')); + assert.equal(input.dataset.rest, 'true'); + + assert.deepEqual(logs, ['get_rest']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/main.svelte new file mode 100644 index 0000000000..313f8c1272 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-spread-and-attribute-directive/main.svelte @@ -0,0 +1,19 @@ + + + focused = true} + onblur={() => focused = false} + class:dark={true} + class={`${focused ? 'focused' : ''}`} + {...get_rest()} +> \ No newline at end of file