Skip to content

Commit 5576114

Browse files
committed
set_class with class: directives
1 parent 9873443 commit 5576114

File tree

11 files changed

+383
-319
lines changed

11 files changed

+383
-319
lines changed

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

Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -767,66 +767,39 @@ export function analyze_component(root, source, options) {
767767
if (!should_ignore_unused) {
768768
warn_unused(analysis.css.ast);
769769
}
770+
}
770771

771-
outer: for (const node of analysis.elements) {
772-
if (node.metadata.scoped) {
773-
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
774-
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
775-
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
776-
777-
/** @type {AST.Attribute | undefined} */
778-
let class_attribute = undefined;
779-
780-
for (const attribute of node.attributes) {
781-
if (attribute.type === 'SpreadAttribute') {
782-
// The spread method appends the hash to the end of the class attribute on its own
783-
continue outer;
784-
}
785-
786-
if (attribute.type !== 'Attribute') continue;
787-
if (attribute.name.toLowerCase() !== 'class') continue;
788-
// The dynamic class method appends the hash to the end of the class attribute on its own
789-
if (attribute.metadata.needs_clsx) continue outer;
772+
for (const node of analysis.elements) {
773+
if (node.metadata.scoped && is_custom_element_node(node)) {
774+
mark_subtree_dynamic(node.metadata.path);
775+
}
790776

791-
class_attribute = attribute;
792-
}
777+
let has_class = false;
778+
let has_spread = false;
779+
let has_class_directive = false;
780+
for (const attribute of node.attributes) {
781+
// The spread method appends the hash to the end of the class attribute on its own
782+
if (attribute.type === 'SpreadAttribute') {
783+
has_spread = true;
784+
break;
785+
}
786+
has_class_directive ||= attribute.type === 'ClassDirective';
787+
has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class';
788+
}
793789

794-
if (class_attribute && class_attribute.value !== true) {
795-
if (is_text_attribute(class_attribute)) {
796-
class_attribute.value[0].data += ` ${analysis.css.hash}`;
797-
} else {
798-
/** @type {AST.Text} */
799-
const css_text = {
800-
type: 'Text',
801-
data: ` ${analysis.css.hash}`,
802-
raw: ` ${analysis.css.hash}`,
803-
start: -1,
804-
end: -1
805-
};
806-
807-
if (Array.isArray(class_attribute.value)) {
808-
class_attribute.value.push(css_text);
809-
} else {
810-
class_attribute.value = [class_attribute.value, css_text];
811-
}
790+
// We need an empty class to generate the set_class() or class="" correctly
791+
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
792+
node.attributes.push(
793+
create_attribute('class', -1, -1, [
794+
{
795+
type: 'Text',
796+
data: '',
797+
raw: '',
798+
start: -1,
799+
end: -1
812800
}
813-
} else {
814-
node.attributes.push(
815-
create_attribute('class', -1, -1, [
816-
{
817-
type: 'Text',
818-
data: analysis.css.hash,
819-
raw: analysis.css.hash,
820-
start: -1,
821-
end: -1
822-
}
823-
])
824-
);
825-
if (is_custom_element_node(node) && node.attributes.length === 1) {
826-
mark_subtree_dynamic(node.metadata.path);
827-
}
828-
}
829-
}
801+
])
802+
);
830803
}
831804
}
832805

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

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */
1+
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -14,15 +14,15 @@ import { escape_html } from '../../../../../escaping.js';
1414
import { dev, is_ignored, locator } from '../../../../state.js';
1515
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
1616
import * as b from '../../../../utils/builders.js';
17-
import { is_custom_element_node } from '../../../nodes.js';
17+
import { create_expression_metadata, is_custom_element_node } from '../../../nodes.js';
1818
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
1919
import { build_getter } from '../utils.js';
2020
import {
2121
get_attribute_name,
2222
build_attribute_value,
23-
build_class_directives,
2423
build_style_directives,
25-
build_set_attributes
24+
build_set_attributes,
25+
build_set_class
2626
} from './shared/element.js';
2727
import { process_children } from './shared/fragment.js';
2828
import {
@@ -223,6 +223,8 @@ export function RegularElement(node, context) {
223223

224224
build_set_attributes(
225225
attributes,
226+
class_directives,
227+
style_directives,
226228
context,
227229
node,
228230
node_id,
@@ -270,13 +272,22 @@ export function RegularElement(node, context) {
270272
continue;
271273
}
272274

275+
const name = get_attribute_name(node, attribute);
273276
if (
274277
!is_custom_element &&
275278
!cannot_be_set_statically(attribute.name) &&
276-
(attribute.value === true || is_text_attribute(attribute))
279+
(attribute.value === true || is_text_attribute(attribute)) &&
280+
(name !== 'class' || class_directives.length === 0)
277281
) {
278-
const name = get_attribute_name(node, attribute);
279-
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
282+
let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
283+
284+
if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) {
285+
if (value === true || value === '') {
286+
value = context.state.analysis.css.hash;
287+
} else {
288+
value += ' ' + context.state.analysis.css.hash;
289+
}
290+
}
280291

281292
if (name !== 'class' || value) {
282293
context.state.template.push(
@@ -290,15 +301,23 @@ export function RegularElement(node, context) {
290301
continue;
291302
}
292303

293-
const is = is_custom_element
294-
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
295-
: build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
304+
const is =
305+
is_custom_element && name !== 'class'
306+
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
307+
: build_element_attribute_update_assignment(
308+
node,
309+
node_id,
310+
attribute,
311+
attributes,
312+
class_directives,
313+
style_directives,
314+
context
315+
);
296316
if (is) is_attributes_reactive = true;
297317
}
298318
}
299319

300-
// class/style directives must be applied last since they could override class/style attributes
301-
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
320+
// style directives must be applied last since they could override class/style attributes
302321
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
303322

304323
if (
@@ -492,6 +511,23 @@ function setup_select_synchronization(value_binding, context) {
492511
);
493512
}
494513

514+
/**
515+
* @param {AST.ClassDirective[]} class_directives
516+
* @param {ComponentContext} context
517+
* @return {ObjectExpression}
518+
*/
519+
export function build_class_directives_object(class_directives, context) {
520+
let properties = [];
521+
for (const d of class_directives) {
522+
let expression = /** @type Expression */ (context.visit(d.expression));
523+
if (d.metadata.expression.has_call) {
524+
expression = get_expression_id(context.state, expression);
525+
}
526+
properties.push(b.init(d.name, expression));
527+
}
528+
return b.object(properties);
529+
}
530+
495531
/**
496532
* Serializes an assignment to an element property by adding relevant statements to either only
497533
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@@ -518,6 +554,8 @@ function setup_select_synchronization(value_binding, context) {
518554
* @param {Identifier} node_id
519555
* @param {AST.Attribute} attribute
520556
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
557+
* @param {AST.ClassDirective[]} class_directives
558+
* @param {AST.StyleDirective[]} style_directives
521559
* @param {ComponentContext} context
522560
* @returns {boolean}
523561
*/
@@ -526,6 +564,8 @@ function build_element_attribute_update_assignment(
526564
node_id,
527565
attribute,
528566
attributes,
567+
class_directives,
568+
style_directives,
529569
context
530570
) {
531571
const state = context.state;
@@ -564,19 +604,15 @@ function build_element_attribute_update_assignment(
564604
let update;
565605

566606
if (name === 'class') {
567-
if (attribute.metadata.needs_clsx) {
568-
value = b.call('$.clsx', value);
569-
}
570-
571-
update = b.stmt(
572-
b.call(
573-
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
574-
node_id,
575-
value,
576-
attribute.metadata.needs_clsx && context.state.analysis.css.hash
577-
? b.literal(context.state.analysis.css.hash)
578-
: undefined
579-
)
607+
return build_set_class(
608+
element,
609+
node_id,
610+
attribute,
611+
value,
612+
has_state,
613+
class_directives,
614+
context,
615+
!is_svg && !is_mathml
580616
);
581617
} else if (name === 'value') {
582618
update = b.stmt(b.call('$.set_value', node_id, value));
@@ -640,14 +676,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
640676
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
641677
let { value, has_state } = build_attribute_value(attribute.value, context);
642678

643-
// We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
644-
if (name === 'class' && attribute.metadata.needs_clsx) {
645-
if (context.state.analysis.css.hash) {
646-
value = b.array([value, b.literal(context.state.analysis.css.hash)]);
647-
}
648-
value = b.call('$.clsx', value);
649-
}
650-
651679
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
652680

653681
if (has_state) {

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import * as b from '../../../../utils/builders.js';
77
import { determine_namespace_for_children } from '../../utils.js';
88
import {
99
build_attribute_value,
10-
build_class_directives,
1110
build_set_attributes,
11+
build_set_class,
1212
build_style_directives
1313
} from './shared/element.js';
14-
import { build_render_statement } from './shared/utils.js';
14+
import { build_render_statement, get_expression_id } from './shared/utils.js';
1515

1616
/**
1717
* @param {AST.SvelteElement} node
@@ -80,19 +80,31 @@ export function SvelteElement(node, context) {
8080
// Then do attributes
8181
let is_attributes_reactive = false;
8282

83-
if (attributes.length === 0) {
84-
if (context.state.analysis.css.hash) {
85-
inner_context.state.init.push(
86-
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
87-
);
88-
}
89-
} else {
83+
if (
84+
attributes.length === 1 &&
85+
attributes[0].type === 'Attribute' &&
86+
attributes[0].name.toLowerCase() === 'class'
87+
) {
88+
// special case when there only a class attribute
89+
build_set_class(
90+
node,
91+
element_id,
92+
attributes[0],
93+
b.null,
94+
false,
95+
class_directives,
96+
inner_context,
97+
false
98+
);
99+
} else if (attributes.length) {
90100
const attributes_id = b.id(context.state.scope.generate('attributes'));
91101

92102
// Always use spread because we don't know whether the element is a custom element or not,
93103
// therefore we need to do the "how to set an attribute" logic at runtime.
94104
is_attributes_reactive = build_set_attributes(
95105
attributes,
106+
class_directives,
107+
style_directives,
96108
inner_context,
97109
node,
98110
element_id,
@@ -103,8 +115,7 @@ export function SvelteElement(node, context) {
103115
);
104116
}
105117

106-
// class/style directives must be applied last since they could override class/style attributes
107-
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
118+
// style directives must be applied last since they could override class/style attributes
108119
build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);
109120

110121
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));

0 commit comments

Comments
 (0)