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