Skip to content

Commit 6eecf16

Browse files
committed
class:"..." for single class: on the client
#7170 / #12610 / #7294 todos: - language-tools support (syntax highlighting & intellisense) - playground syntax highlighting? - ssr
1 parent ac9b7de commit 6eecf16

File tree

6 files changed

+95
-12
lines changed

6 files changed

+95
-12
lines changed

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ function read_attribute(parser) {
496496
}
497497
}
498498

499-
const name = parser.read_until(regex_token_ending_character);
499+
let name = parser.read_until(regex_token_ending_character);
500500
if (!name) return null;
501501

502502
let end = parser.index;
@@ -512,15 +512,32 @@ function read_attribute(parser) {
512512
parser.allow_whitespace();
513513
value = read_attribute_value(parser);
514514
end = parser.index;
515-
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
515+
} else if (parser.match_regex(regex_starts_with_quote_characters) && type !== 'ClassDirective') {
516516
e.expected_token(parser.index, '=');
517517
}
518518

519519
if (type) {
520520
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
521+
/** @type {Array<AST.Text | AST.ExpressionTag> | null} */
522+
let class_name_value = null;
521523

522524
if (directive_name === '') {
523-
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
525+
if (type !== 'ClassDirective') {
526+
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
527+
} else {
528+
// class:"..." case
529+
const v = read_attribute_value(parser);
530+
if (!Array.isArray(v)) e.expected_token(v.start, '" or =');
531+
class_name_value = v;
532+
533+
if (parser.eat('=')) {
534+
parser.allow_whitespace();
535+
value = read_attribute_value(parser);
536+
end = parser.index;
537+
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
538+
e.expected_token(parser.index, '=');
539+
}
540+
}
524541
}
525542

526543
if (type === 'StyleDirective') {
@@ -569,6 +586,10 @@ function read_attribute(parser) {
569586
}
570587
};
571588

589+
if (directive.type === 'ClassDirective') {
590+
directive.value = class_name_value;
591+
}
592+
572593
if (directive.type === 'TransitionDirective') {
573594
const direction = name.slice(0, colon_index);
574595
directive.intro = direction === 'in' || direction === 'transition';

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,13 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
627627
if (
628628
!attribute_matches(element, 'class', name, '~=', false) &&
629629
!element.attributes.some(
630-
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
630+
(attribute) =>
631+
attribute.type === 'ClassDirective' &&
632+
(attribute.name === name ||
633+
(attribute.value &&
634+
(attribute.value.length > 1 ||
635+
attribute.value[0].type !== 'Text' ||
636+
test_attribute('~=', name, false, attribute.value[0].data))))
631637
)
632638
) {
633639
return false;

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
2-
/** @import { AST, Namespace } from '#compiler' */
1+
/** @import { Expression, ExpressionStatement, Identifier, ObjectExpression } from 'estree' */
2+
/** @import { AST } from '#compiler' */
33
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
44
import { normalize_attribute } from '../../../../../../utils.js';
55
import { is_ignored } from '../../../../../state.js';
6-
import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js';
6+
import { is_event_attribute } from '../../../../../utils/ast.js';
77
import * as b from '../../../../../utils/builders.js';
88
import { build_getter, create_derived } from '../../utils.js';
99
import { build_template_literal, build_update } from './utils.js';
@@ -154,7 +154,7 @@ export function build_class_directives(
154154
) {
155155
const state = context.state;
156156
for (const directive of class_directives) {
157-
const { has_state, has_call } = directive.metadata.expression;
157+
let { has_state, has_call } = directive.metadata.expression;
158158
let value = /** @type {Expression} */ (context.visit(directive.expression));
159159

160160
if (has_call) {
@@ -164,7 +164,37 @@ export function build_class_directives(
164164
value = b.call('$.get', id);
165165
}
166166

167-
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
167+
/** @type {ExpressionStatement} */
168+
let update;
169+
170+
if (!directive.value) {
171+
update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
172+
} else {
173+
let prev_id;
174+
let {
175+
has_call: h_c,
176+
has_state: h_s,
177+
value: name
178+
} = build_attribute_value(directive.value, context);
179+
180+
has_call ||= h_c;
181+
has_state ||= h_s;
182+
183+
if (h_c || h_s) {
184+
prev_id = b.id(state.scope.generate('prev_class_names'));
185+
state.init.push(b.let(prev_id, b.literal('')));
186+
}
187+
188+
if (has_call) {
189+
const id = b.id(state.scope.generate('class_directive'));
190+
191+
state.init.push(b.const(id, create_derived(state, b.thunk(name))));
192+
name = b.call('$.get', id);
193+
}
194+
195+
const toggle = b.call('$.toggle_classes', element_id, name, value, prev_id);
196+
update = b.stmt(prev_id ? b.assignment('=', prev_id, toggle) : toggle);
197+
}
168198

169199
if (!is_attributes_reactive && has_call) {
170200
state.init.push(build_update(update));

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,10 @@ export namespace AST {
196196
/** A `class:` directive */
197197
export interface ClassDirective extends BaseNode {
198198
type: 'ClassDirective';
199-
/** The 'x' in `class:x` */
200-
name: 'class';
199+
/** The 'x' in `class:x`, empty string in case of `class:"..."` */
200+
name: string;
201+
/** The 'x' in `class:"x"`, `null` in case of `class:x` (i.e. when no quotes) */
202+
value: Array<Text | ExpressionTag> | null;
201203
/** The 'y' in `class:x={y}`, or the `x` in `class:x` */
202204
expression: Expression;
203205
/** @internal */

packages/svelte/src/internal/client/dom/elements/class.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,21 @@ export function toggle_class(dom, class_name, value) {
114114
dom.classList.remove(class_name);
115115
}
116116
}
117+
118+
/**
119+
* @param {Element} dom
120+
* @param {string} class_names
121+
* @param {boolean} value
122+
* @param {string} prev_class_names
123+
* @returns {string}
124+
*/
125+
export function toggle_classes(dom, class_names, value, prev_class_names) {
126+
const split_classes = class_names.split(' ');
127+
split_classes.forEach((class_name) => toggle_class(dom, class_name, value));
128+
prev_class_names.split(' ').forEach((class_name) => {
129+
if (!split_classes.includes(class_name)) {
130+
toggle_class(dom, class_name, false);
131+
}
132+
});
133+
return class_names;
134+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ export {
3535
set_value,
3636
set_checked
3737
} from './dom/elements/attributes.js';
38-
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
38+
export {
39+
set_class,
40+
set_svg_class,
41+
set_mathml_class,
42+
toggle_class,
43+
toggle_classes
44+
} from './dom/elements/class.js';
3945
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
4046
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
4147
export { set_style } from './dom/elements/style.js';

0 commit comments

Comments
 (0)