Skip to content

Commit 30562b8

Browse files
adigubadummdidummRich-Harris
authored
chore: rewrite set_style() to handle directives (#15418)
* add style attribute when needed * set_style() * to_style() * remove `style=""` * use cssTest for perfs * base test * test * changeset * revert dom.style.cssText * format name * use style.cssText + adapt test * Apply suggestions from code review suggestions from dummdidumm Co-authored-by: Simon H <[email protected]> * fix priority * lint * yawn * update test * we can simplify some stuff now * simplify * more * simplify some more * more * more * more * more * more * remove continue * tweak * tweak * tweak * skip hash argument where possible * tweak * tweak * tweak * tweak --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 2f685c1 commit 30562b8

File tree

19 files changed

+654
-399
lines changed

19 files changed

+654
-399
lines changed

Diff for: .changeset/strange-planes-shout.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: make `style:` directive and CSS handling more robust

Diff for: packages/svelte/src/compiler/phases/2-analyze/index.js

+24-2
Original file line numberDiff line numberDiff line change
@@ -769,17 +769,24 @@ export function analyze_component(root, source, options) {
769769
}
770770

771771
let has_class = false;
772+
let has_style = false;
772773
let has_spread = false;
773774
let has_class_directive = false;
775+
let has_style_directive = false;
774776

775777
for (const attribute of node.attributes) {
776778
// The spread method appends the hash to the end of the class attribute on its own
777779
if (attribute.type === 'SpreadAttribute') {
778780
has_spread = true;
779781
break;
782+
} else if (attribute.type === 'Attribute') {
783+
has_class ||= attribute.name.toLowerCase() === 'class';
784+
has_style ||= attribute.name.toLowerCase() === 'style';
785+
} else if (attribute.type === 'ClassDirective') {
786+
has_class_directive = true;
787+
} else if (attribute.type === 'StyleDirective') {
788+
has_style_directive = true;
780789
}
781-
has_class_directive ||= attribute.type === 'ClassDirective';
782-
has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class';
783790
}
784791

785792
// We need an empty class to generate the set_class() or class="" correctly
@@ -796,6 +803,21 @@ export function analyze_component(root, source, options) {
796803
])
797804
);
798805
}
806+
807+
// We need an empty style to generate the set_style() correctly
808+
if (!has_spread && !has_style && has_style_directive) {
809+
node.attributes.push(
810+
create_attribute('style', -1, -1, [
811+
{
812+
type: 'Text',
813+
data: '',
814+
raw: '',
815+
start: -1,
816+
end: -1
817+
}
818+
])
819+
);
820+
}
799821
}
800822

801823
// TODO

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

+105-125
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
1+
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -20,9 +20,9 @@ import { build_getter } from '../utils.js';
2020
import {
2121
get_attribute_name,
2222
build_attribute_value,
23-
build_style_directives,
2423
build_set_attributes,
25-
build_set_class
24+
build_set_class,
25+
build_set_style
2626
} from './shared/element.js';
2727
import { process_children } from './shared/fragment.js';
2828
import {
@@ -215,13 +215,18 @@ export function RegularElement(node, context) {
215215

216216
const node_id = context.state.node;
217217

218-
// Then do attributes
219-
let is_attributes_reactive = has_spread;
220-
221218
if (has_spread) {
222219
const attributes_id = b.id(context.state.scope.generate('attributes'));
223220

224-
build_set_attributes(attributes, class_directives, context, node, node_id, attributes_id);
221+
build_set_attributes(
222+
attributes,
223+
class_directives,
224+
style_directives,
225+
context,
226+
node,
227+
node_id,
228+
attributes_id
229+
);
225230

226231
// If value binding exists, that one takes care of calling $.init_select
227232
if (node.name === 'select' && !bindings.has('value')) {
@@ -262,11 +267,13 @@ export function RegularElement(node, context) {
262267
}
263268

264269
const name = get_attribute_name(node, attribute);
270+
265271
if (
266272
!is_custom_element &&
267273
!cannot_be_set_statically(attribute.name) &&
268274
(attribute.value === true || is_text_attribute(attribute)) &&
269-
(name !== 'class' || class_directives.length === 0)
275+
(name !== 'class' || class_directives.length === 0) &&
276+
(name !== 'style' || style_directives.length === 0)
270277
) {
271278
let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
272279

@@ -287,27 +294,30 @@ export function RegularElement(node, context) {
287294
}`
288295
);
289296
}
290-
continue;
291-
}
297+
} else if (name === 'autofocus') {
298+
let { value } = build_attribute_value(attribute.value, context);
299+
context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
300+
} else if (name === 'class') {
301+
const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg';
302+
build_set_class(node, node_id, attribute, class_directives, context, is_html);
303+
} else if (name === 'style') {
304+
build_set_style(node_id, attribute, style_directives, context);
305+
} else if (is_custom_element) {
306+
build_custom_element_attribute_update_assignment(node_id, attribute, context);
307+
} else {
308+
const { value, has_state } = build_attribute_value(
309+
attribute.value,
310+
context,
311+
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
312+
);
313+
314+
const update = build_element_attribute_update(node, node_id, name, value, attributes);
292315

293-
const is =
294-
is_custom_element && name !== 'class'
295-
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
296-
: build_element_attribute_update_assignment(
297-
node,
298-
node_id,
299-
attribute,
300-
attributes,
301-
class_directives,
302-
context
303-
);
304-
if (is) is_attributes_reactive = true;
316+
(has_state ? context.state.update : context.state.init).push(b.stmt(update));
317+
}
305318
}
306319
}
307320

308-
// style directives must be applied last since they could override class/style attributes
309-
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
310-
311321
if (
312322
is_load_error_element(node.name) &&
313323
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))
@@ -519,6 +529,36 @@ export function build_class_directives_object(class_directives, context) {
519529
return b.object(properties);
520530
}
521531

532+
/**
533+
* @param {AST.StyleDirective[]} style_directives
534+
* @param {ComponentContext} context
535+
* @return {ObjectExpression | ArrayExpression}}
536+
*/
537+
export function build_style_directives_object(style_directives, context) {
538+
let normal_properties = [];
539+
let important_properties = [];
540+
541+
for (const directive of style_directives) {
542+
const expression =
543+
directive.value === true
544+
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
545+
: build_attribute_value(directive.value, context, (value, metadata) =>
546+
metadata.has_call ? get_expression_id(context.state, value) : value
547+
).value;
548+
const property = b.init(directive.name, expression);
549+
550+
if (directive.modifiers.includes('important')) {
551+
important_properties.push(property);
552+
} else {
553+
normal_properties.push(property);
554+
}
555+
}
556+
557+
return important_properties.length
558+
? b.array([b.object(normal_properties), b.object(important_properties)])
559+
: b.object(normal_properties);
560+
}
561+
522562
/**
523563
* Serializes an assignment to an element property by adding relevant statements to either only
524564
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@@ -543,73 +583,29 @@ export function build_class_directives_object(class_directives, context) {
543583
* Returns true if attribute is deemed reactive, false otherwise.
544584
* @param {AST.RegularElement} element
545585
* @param {Identifier} node_id
546-
* @param {AST.Attribute} attribute
586+
* @param {string} name
587+
* @param {Expression} value
547588
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
548-
* @param {AST.ClassDirective[]} class_directives
549-
* @param {ComponentContext} context
550-
* @returns {boolean}
551589
*/
552-
function build_element_attribute_update_assignment(
553-
element,
554-
node_id,
555-
attribute,
556-
attributes,
557-
class_directives,
558-
context
559-
) {
560-
const state = context.state;
561-
const name = get_attribute_name(element, attribute);
562-
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
563-
const is_mathml = context.state.metadata.namespace === 'mathml';
564-
565-
const is_autofocus = name === 'autofocus';
566-
567-
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
568-
metadata.has_call
569-
? // if it's autofocus we will not add this to a template effect so we don't want to get the expression id
570-
// but separately memoize the expression
571-
is_autofocus
572-
? memoize_expression(state, value)
573-
: get_expression_id(state, value)
574-
: value
575-
);
590+
function build_element_attribute_update(element, node_id, name, value, attributes) {
591+
if (name === 'muted') {
592+
// Special case for Firefox who needs it set as a property in order to work
593+
return b.assignment('=', b.member(node_id, b.id('muted')), value);
594+
}
576595

577-
if (is_autofocus) {
578-
state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
579-
return false;
596+
if (name === 'value') {
597+
return b.call('$.set_value', node_id, value);
580598
}
581599

582-
// Special case for Firefox who needs it set as a property in order to work
583-
if (name === 'muted') {
584-
if (!has_state) {
585-
state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
586-
return false;
587-
}
588-
state.update.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
589-
return false;
600+
if (name === 'checked') {
601+
return b.call('$.set_checked', node_id, value);
590602
}
591603

592-
/** @type {Statement} */
593-
let update;
604+
if (name === 'selected') {
605+
return b.call('$.set_selected', node_id, value);
606+
}
594607

595-
if (name === 'class') {
596-
return build_set_class(
597-
element,
598-
node_id,
599-
attribute,
600-
value,
601-
has_state,
602-
class_directives,
603-
context,
604-
!is_svg && !is_mathml
605-
);
606-
} else if (name === 'value') {
607-
update = b.stmt(b.call('$.set_value', node_id, value));
608-
} else if (name === 'checked') {
609-
update = b.stmt(b.call('$.set_checked', node_id, value));
610-
} else if (name === 'selected') {
611-
update = b.stmt(b.call('$.set_selected', node_id, value));
612-
} else if (
608+
if (
613609
// If we would just set the defaultValue property, it would override the value property,
614610
// because it is set in the template which implicitly means it's also setting the default value,
615611
// and if one updates the default value while the input is pristine it will also update the
@@ -620,62 +616,49 @@ function build_element_attribute_update_assignment(
620616
) ||
621617
(element.name === 'textarea' && element.fragment.nodes.length > 0))
622618
) {
623-
update = b.stmt(b.call('$.set_default_value', node_id, value));
624-
} else if (
619+
return b.call('$.set_default_value', node_id, value);
620+
}
621+
622+
if (
625623
// See defaultValue comment
626624
name === 'defaultChecked' &&
627625
attributes.some(
628626
(attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
629627
)
630628
) {
631-
update = b.stmt(b.call('$.set_default_checked', node_id, value));
632-
} else if (is_dom_property(name)) {
633-
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
634-
} else {
635-
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
636-
update = b.stmt(
637-
b.call(
638-
callee,
639-
node_id,
640-
b.literal(name),
641-
value,
642-
is_ignored(element, 'hydration_attribute_changed') && b.true
643-
)
644-
);
629+
return b.call('$.set_default_checked', node_id, value);
645630
}
646631

647-
if (has_state) {
648-
state.update.push(update);
649-
return true;
650-
} else {
651-
state.init.push(update);
652-
return false;
632+
if (is_dom_property(name)) {
633+
return b.assignment('=', b.member(node_id, name), value);
653634
}
635+
636+
return b.call(
637+
name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute',
638+
node_id,
639+
b.literal(name),
640+
value,
641+
is_ignored(element, 'hydration_attribute_changed') && b.true
642+
);
654643
}
655644

656645
/**
657-
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.
646+
* Like `build_element_attribute_update` but without any special attribute treatment.
658647
* @param {Identifier} node_id
659648
* @param {AST.Attribute} attribute
660649
* @param {ComponentContext} context
661-
* @returns {boolean}
662650
*/
663651
function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
664-
const state = context.state;
665-
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
666-
let { value, has_state } = build_attribute_value(attribute.value, context);
652+
const { value, has_state } = build_attribute_value(attribute.value, context);
667653

668-
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
654+
// don't lowercase name, as we set the element's property, which might be case sensitive
655+
const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value);
669656

670-
if (has_state) {
671-
// this is different from other updates — it doesn't get grouped,
672-
// because set_custom_element_data may not be idempotent
673-
state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression))));
674-
return true;
675-
} else {
676-
state.init.push(update);
677-
return false;
678-
}
657+
// this is different from other updates — it doesn't get grouped,
658+
// because set_custom_element_data may not be idempotent
659+
const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call;
660+
661+
context.state.init.push(b.stmt(update));
679662
}
680663

681664
/**
@@ -686,7 +669,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
686669
* @param {Identifier} node_id
687670
* @param {AST.Attribute} attribute
688671
* @param {ComponentContext} context
689-
* @returns {boolean}
690672
*/
691673
function build_element_special_value_attribute(element, node_id, attribute, context) {
692674
const state = context.state;
@@ -699,7 +681,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
699681
metadata.has_call
700682
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
701683
is_select_with_value
702-
? memoize_expression(context.state, value)
684+
? memoize_expression(state, value)
703685
: get_expression_id(state, value)
704686
: value
705687
);
@@ -743,9 +725,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
743725
value,
744726
update
745727
);
746-
return true;
747728
} else {
748729
state.init.push(update);
749-
return false;
750730
}
751731
}

0 commit comments

Comments
 (0)