Skip to content

Commit b6a67e8

Browse files
fix: prevent spread attribute from overriding class directive (#13763)
1 parent de609ec commit b6a67e8

File tree

6 files changed

+65
-15
lines changed

6 files changed

+65
-15
lines changed

.changeset/red-coats-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: prevent spread attribute from overriding class directive

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ export function RegularElement(node, context) {
219219
node_id,
220220
attributes_id,
221221
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
222-
node.name.includes('-') && b.true
222+
node.name.includes('-') && b.true,
223+
context.state
223224
);
224225

225226
// If value binding exists, that one takes care of calling $.init_select

packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ export function SvelteElement(node, context) {
102102
element_id,
103103
attributes_id,
104104
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
105-
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
105+
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')),
106+
context.state
106107
);
107108
}
108109

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
22
/** @import { AST, Namespace } from '#compiler' */
3-
/** @import { ComponentContext } from '../../types' */
3+
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
44
import { normalize_attribute } from '../../../../../../utils.js';
55
import { is_ignored } from '../../../../../state.js';
66
import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js';
@@ -16,6 +16,7 @@ import { build_template_literal, build_update } from './utils.js';
1616
* @param {Identifier} attributes_id
1717
* @param {false | Expression} preserve_attribute_case
1818
* @param {false | Expression} is_custom_element
19+
* @param {ComponentClientTransformState} state
1920
*/
2021
export function build_set_attributes(
2122
attributes,
@@ -24,9 +25,9 @@ export function build_set_attributes(
2425
element_id,
2526
attributes_id,
2627
preserve_attribute_case,
27-
is_custom_element
28+
is_custom_element,
29+
state
2830
) {
29-
let needs_isolation = false;
3031
let has_state = false;
3132

3233
/** @type {ObjectExpression['properties']} */
@@ -50,12 +51,17 @@ export function build_set_attributes(
5051

5152
has_state ||= attribute.metadata.expression.has_state;
5253
} else {
53-
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
54-
5554
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
5655
has_state = true;
5756

58-
needs_isolation ||= attribute.metadata.expression.has_call;
57+
let value = /** @type {Expression} */ (context.visit(attribute));
58+
59+
if (attribute.metadata.expression.has_call) {
60+
const id = b.id(state.scope.generate('spread_with_call'));
61+
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
62+
value = b.call('$.get', id);
63+
}
64+
values.push(b.spread(value));
5965
}
6066
}
6167

@@ -72,14 +78,7 @@ export function build_set_attributes(
7278

7379
if (has_state) {
7480
context.state.init.push(b.let(attributes_id));
75-
7681
const update = b.stmt(b.assignment('=', attributes_id, call));
77-
78-
if (needs_isolation) {
79-
context.state.init.push(build_update(update));
80-
return false;
81-
}
82-
8382
context.state.update.push(update);
8483
return true;
8584
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { flushSync } from 'svelte';
2+
import { test, ok } from '../../test';
3+
4+
export default test({
5+
test({ target, logs, assert }) {
6+
const input = target.querySelector('input');
7+
8+
ok(input);
9+
10+
assert.deepEqual(logs, ['get_rest']);
11+
12+
assert.ok(input.classList.contains('dark'));
13+
assert.equal(input.dataset.rest, 'true');
14+
15+
flushSync(() => {
16+
input.focus();
17+
});
18+
19+
assert.ok(input.classList.contains('dark'));
20+
assert.ok(input.classList.contains('focused'));
21+
assert.equal(input.dataset.rest, 'true');
22+
23+
assert.deepEqual(logs, ['get_rest']);
24+
}
25+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
let focused = $state(false)
3+
4+
function get_rest() {
5+
console.log("get_rest");
6+
return {
7+
"data-rest": "true"
8+
}
9+
}
10+
11+
</script>
12+
13+
<input
14+
onfocus={() => focused = true}
15+
onblur={() => focused = false}
16+
class:dark={true}
17+
class={`${focused ? 'focused' : ''}`}
18+
{...get_rest()}
19+
>

0 commit comments

Comments
 (0)