Skip to content

Commit b0511a5

Browse files
authored
fix: improve attribute directive reactivity detection (#9907)
1 parent 4e61db7 commit b0511a5

File tree

5 files changed

+128
-39
lines changed

5 files changed

+128
-39
lines changed

.changeset/happy-suits-film.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: improve attribute directive reactivity detection

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

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -63,32 +63,48 @@ function get_attribute_name(element, attribute, context) {
6363
* @param {boolean} is_attributes_reactive
6464
*/
6565
function serialize_style_directives(style_directives, element_id, context, is_attributes_reactive) {
66-
if (style_directives.length > 0) {
67-
const values = style_directives.map((directive) => {
68-
let value =
69-
directive.value === true
70-
? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state)
71-
: serialize_attribute_value(directive.value, context)[1];
72-
return b.stmt(
73-
b.call(
74-
'$.style',
75-
element_id,
76-
b.literal(directive.name),
77-
value,
78-
/** @type {import('estree').Expression} */ (
79-
directive.modifiers.includes('important') ? b.true : undefined
80-
)
66+
const state = context.state;
67+
68+
for (const directive of style_directives) {
69+
let value =
70+
directive.value === true
71+
? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state)
72+
: serialize_attribute_value(directive.value, context)[1];
73+
const grouped = b.stmt(
74+
b.call(
75+
'$.style',
76+
element_id,
77+
b.literal(directive.name),
78+
value,
79+
/** @type {import('estree').Expression} */ (
80+
directive.modifiers.includes('important') ? b.true : undefined
81+
)
82+
)
83+
);
84+
const singular = b.stmt(
85+
b.call(
86+
'$.style_effect',
87+
element_id,
88+
b.literal(directive.name),
89+
b.arrow([], value),
90+
/** @type {import('estree').Expression} */ (
91+
directive.modifiers.includes('important') ? b.true : undefined
8192
)
93+
)
94+
);
95+
96+
const contains_call_expression =
97+
Array.isArray(directive.value) &&
98+
directive.value.some(
99+
(v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression
82100
);
83-
});
84101

85-
if (
86-
is_attributes_reactive ||
87-
style_directives.some((directive) => directive.metadata.dynamic)
88-
) {
89-
context.state.update.push(...values.map((v) => ({ grouped: v })));
102+
if (!is_attributes_reactive && contains_call_expression) {
103+
state.update_effects.push(singular);
104+
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
105+
state.update.push({ grouped, singular });
90106
} else {
91-
context.state.init.push(...values);
107+
state.init.push(grouped);
92108
}
93109
}
94110
}
@@ -123,21 +139,21 @@ function parse_directive_name(name) {
123139
* @param {boolean} is_attributes_reactive
124140
*/
125141
function serialize_class_directives(class_directives, element_id, context, is_attributes_reactive) {
126-
if (class_directives.length > 0) {
127-
const values = class_directives.map((directive) => {
128-
const value = /** @type {import('estree').Expression} */ (
129-
context.visit(directive.expression)
130-
);
131-
return b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value));
132-
});
142+
const state = context.state;
143+
for (const directive of class_directives) {
144+
const value = /** @type {import('estree').Expression} */ (context.visit(directive.expression));
145+
const grouped = b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value));
146+
const singular = b.stmt(
147+
b.call('$.class_toggle_effect', element_id, b.literal(directive.name), b.arrow([], value))
148+
);
149+
const contains_call_expression = directive.expression.type === 'CallExpression';
133150

134-
if (
135-
is_attributes_reactive ||
136-
class_directives.some((directive) => directive.metadata.dynamic)
137-
) {
138-
context.state.update.push(...values.map((v) => ({ grouped: v })));
151+
if (!is_attributes_reactive && contains_call_expression) {
152+
state.update_effects.push(singular);
153+
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
154+
state.update.push({ grouped, singular });
139155
} else {
140-
context.state.init.push(...values);
156+
state.init.push(grouped);
141157
}
142158
}
143159
}
@@ -295,7 +311,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
295311
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
296312
}
297313

298-
is_reactive ||= attribute.metadata.dynamic;
314+
is_reactive ||=
315+
attribute.metadata.dynamic ||
316+
(attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression);
299317
}
300318

301319
const lowercase_attributes =

packages/svelte/src/internal/client/render.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,20 @@ export function class_toggle(dom, class_name, value) {
444444
dom.classList.remove(class_name);
445445
}
446446
}
447+
448+
/**
449+
* @param {Element} dom
450+
* @param {string} class_name
451+
* @param {() => boolean} value
452+
* @returns {void}
453+
*/
454+
export function class_toggle_effect(dom, class_name, value) {
455+
render_effect(() => {
456+
const string = value();
457+
class_toggle(dom, class_name, string);
458+
});
459+
}
460+
447461
/**
448462
* Selects the correct option(s) (depending on whether this is a multiple select)
449463
* @template V
@@ -2359,13 +2373,31 @@ export function set_custom_element_data(node, prop, value) {
23592373
* @param {boolean} [important]
23602374
*/
23612375
export function style(dom, key, value, important) {
2376+
const style = dom.style;
2377+
const prev_value = style.getPropertyValue(key);
23622378
if (value == null) {
2363-
dom.style.removeProperty(key);
2364-
} else {
2365-
dom.style.setProperty(key, value, important ? 'important' : '');
2379+
if (prev_value !== '') {
2380+
style.removeProperty(key);
2381+
}
2382+
} else if (prev_value !== value) {
2383+
style.setProperty(key, value, important ? 'important' : '');
23662384
}
23672385
}
23682386

2387+
/**
2388+
* @param {HTMLElement} dom
2389+
* @param {string} key
2390+
* @param {() => string} value
2391+
* @param {boolean} [important]
2392+
* @returns {void}
2393+
*/
2394+
export function style_effect(dom, key, value, important) {
2395+
render_effect(() => {
2396+
const string = value();
2397+
style(dom, key, string, important);
2398+
});
2399+
}
2400+
23692401
/**
23702402
* List of attributes that should always be set through the attr method,
23712403
* because updating them through the property setter doesn't work reliably.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<div style="color: red;"></div><div class="red"></div><button>toggle</button`,
5+
6+
async test({ assert, target }) {
7+
const [b1] = target.querySelectorAll('button');
8+
9+
b1?.click();
10+
await Promise.resolve();
11+
assert.htmlEqual(
12+
target.innerHTML,
13+
'<div class="blue" style="color: blue;"></div><div class="blue"></div><button>toggle</button>'
14+
);
15+
}
16+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
let value = $state('red');
3+
4+
const getValue = () => {
5+
return value;
6+
}
7+
const getClass = () => {
8+
return value === 'blue';
9+
}
10+
const getSpread = () => {
11+
return { class: value };
12+
}
13+
</script>
14+
15+
<div class:blue={getClass()} style:color={getValue()}></div>
16+
<div {...getSpread()}></div>
17+
<button on:click={() => value = 'blue'}>toggle</button>
18+

0 commit comments

Comments
 (0)