class:"..." for single class: on the client

#7170 / #12610 / #7294

todos:
- language-tools support (syntax highlighting & intellisense)
- playground syntax highlighting?
- ssr
class-directive-enhancements
Simon Holthausen 10 months ago
parent ac9b7de058
commit 6eecf16821

@ -496,7 +496,7 @@ function read_attribute(parser) {
} }
} }
const name = parser.read_until(regex_token_ending_character); let name = parser.read_until(regex_token_ending_character);
if (!name) return null; if (!name) return null;
let end = parser.index; let end = parser.index;
@ -512,15 +512,32 @@ function read_attribute(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
} else if (parser.match_regex(regex_starts_with_quote_characters)) { } else if (parser.match_regex(regex_starts_with_quote_characters) && type !== 'ClassDirective') {
e.expected_token(parser.index, '='); e.expected_token(parser.index, '=');
} }
if (type) { if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|'); const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
/** @type {Array<AST.Text | AST.ExpressionTag> | null} */
let class_name_value = null;
if (directive_name === '') { if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name); if (type !== 'ClassDirective') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
} else {
// class:"..." case
const v = read_attribute_value(parser);
if (!Array.isArray(v)) e.expected_token(v.start, '" or =');
class_name_value = v;
if (parser.eat('=')) {
parser.allow_whitespace();
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '=');
}
}
} }
if (type === 'StyleDirective') { if (type === 'StyleDirective') {
@ -569,6 +586,10 @@ function read_attribute(parser) {
} }
}; };
if (directive.type === 'ClassDirective') {
directive.value = class_name_value;
}
if (directive.type === 'TransitionDirective') { if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index); const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition'; directive.intro = direction === 'in' || direction === 'transition';

@ -627,7 +627,13 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if ( if (
!attribute_matches(element, 'class', name, '~=', false) && !attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some( !element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name (attribute) =>
attribute.type === 'ClassDirective' &&
(attribute.name === name ||
(attribute.value &&
(attribute.value.length > 1 ||
attribute.value[0].type !== 'Text' ||
test_attribute('~=', name, false, attribute.value[0].data))))
) )
) { ) {
return false; return false;

@ -1,9 +1,9 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { Expression, ExpressionStatement, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { normalize_attribute } from '../../../../../../utils.js'; import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js'; import { is_ignored } from '../../../../../state.js';
import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { build_getter, create_derived } from '../../utils.js'; import { build_getter, create_derived } from '../../utils.js';
import { build_template_literal, build_update } from './utils.js'; import { build_template_literal, build_update } from './utils.js';
@ -154,7 +154,7 @@ export function build_class_directives(
) { ) {
const state = context.state; const state = context.state;
for (const directive of class_directives) { for (const directive of class_directives) {
const { has_state, has_call } = directive.metadata.expression; let { has_state, has_call } = directive.metadata.expression;
let value = /** @type {Expression} */ (context.visit(directive.expression)); let value = /** @type {Expression} */ (context.visit(directive.expression));
if (has_call) { if (has_call) {
@ -164,7 +164,37 @@ export function build_class_directives(
value = b.call('$.get', id); value = b.call('$.get', id);
} }
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); /** @type {ExpressionStatement} */
let update;
if (!directive.value) {
update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
} else {
let prev_id;
let {
has_call: h_c,
has_state: h_s,
value: name
} = build_attribute_value(directive.value, context);
has_call ||= h_c;
has_state ||= h_s;
if (h_c || h_s) {
prev_id = b.id(state.scope.generate('prev_class_names'));
state.init.push(b.let(prev_id, b.literal('')));
}
if (has_call) {
const id = b.id(state.scope.generate('class_directive'));
state.init.push(b.const(id, create_derived(state, b.thunk(name))));
name = b.call('$.get', id);
}
const toggle = b.call('$.toggle_classes', element_id, name, value, prev_id);
update = b.stmt(prev_id ? b.assignment('=', prev_id, toggle) : toggle);
}
if (!is_attributes_reactive && has_call) { if (!is_attributes_reactive && has_call) {
state.init.push(build_update(update)); state.init.push(build_update(update));

@ -196,8 +196,10 @@ export namespace AST {
/** A `class:` directive */ /** A `class:` directive */
export interface ClassDirective extends BaseNode { export interface ClassDirective extends BaseNode {
type: 'ClassDirective'; type: 'ClassDirective';
/** The 'x' in `class:x` */ /** The 'x' in `class:x`, empty string in case of `class:"..."` */
name: 'class'; name: string;
/** The 'x' in `class:"x"`, `null` in case of `class:x` (i.e. when no quotes) */
value: Array<Text | ExpressionTag> | null;
/** The 'y' in `class:x={y}`, or the `x` in `class:x` */ /** The 'y' in `class:x={y}`, or the `x` in `class:x` */
expression: Expression; expression: Expression;
/** @internal */ /** @internal */

@ -114,3 +114,21 @@ export function toggle_class(dom, class_name, value) {
dom.classList.remove(class_name); dom.classList.remove(class_name);
} }
} }
/**
* @param {Element} dom
* @param {string} class_names
* @param {boolean} value
* @param {string} prev_class_names
* @returns {string}
*/
export function toggle_classes(dom, class_names, value, prev_class_names) {
const split_classes = class_names.split(' ');
split_classes.forEach((class_name) => toggle_class(dom, class_name, value));
prev_class_names.split(' ').forEach((class_name) => {
if (!split_classes.includes(class_name)) {
toggle_class(dom, class_name, false);
}
});
return class_names;
}

@ -35,7 +35,13 @@ export {
set_value, set_value,
set_checked set_checked
} from './dom/elements/attributes.js'; } from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js'; export {
set_class,
set_svg_class,
set_mathml_class,
toggle_class,
toggle_classes
} from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js'; export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js'; export { set_style } from './dom/elements/style.js';

Loading…
Cancel
Save