Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.2.3",
"is-reference": "^3.0.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,8 @@ export function analyze_component(root, source, options) {

if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;

class_attribute = attribute;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ export function Attribute(node, context) {
mark_subtree_dynamic(context.path);
}

// class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes
if (
node.name === 'class' &&
!Array.isArray(node.value) &&
node.value !== true &&
node.value.expression.type !== 'Literal' &&
node.value.expression.type !== 'TemplateLiteral' &&
node.value.expression.type !== 'BinaryExpression'
) {
mark_subtree_dynamic(context.path);
node.metadata.needs_clsx = true;
}

if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,10 @@ function build_element_attribute_update_assignment(
let update;

if (name === 'class') {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}

if (attribute.metadata.expression.has_state && has_call) {
// ensure we're not creating a separate template effect for this so that
// potential class directives are added to the same effect and therefore always apply
Expand All @@ -561,11 +565,13 @@ function build_element_attribute_update_assignment(
value = b.call('$.get', id);
has_call = false;
}

update = b.stmt(
b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id,
value
value,
attribute.metadata.needs_clsx ? b.literal(context.state.analysis.css.hash) : undefined
)
);
} else if (name === 'value') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,35 @@ export function build_element_attributes(node, context) {
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
class_index = attributes.length;
} else if (attribute.name === 'style') {
style_index = attributes.length;

if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash
? b.binary(
'+',
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
)
: clsx_value
}
});
} else {
attributes.push(attribute);
}
} else {
if (attribute.name === 'style') {
style_index = attributes.length;
}

attributes.push(attribute);
}
attributes.push(attribute);
}
} else if (attribute.type === 'BindDirective') {
if (attribute.name === 'value' && node.name === 'select') continue;
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export function create_attribute(name, start, end, value) {
value,
metadata: {
expression: create_expression_metadata(),
delegated: null
delegated: null,
needs_clsx: false
}
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ export namespace AST {
expression: ExpressionMetadata;
/** May be set if this is an event attribute */
delegated: null | DelegatedEvent;
/** May be `true` if this is a `class` attribute that needs `clsx` */
needs_clsx: boolean;
};
}

Expand Down
22 changes: 13 additions & 9 deletions packages/svelte/src/internal/client/dom/elements/class.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { hydrating } from '../hydration.js';
/**
* @param {SVGElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_svg_class(dom, value) {
export function set_svg_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
var next_class_name = to_class(value, hash);

if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
Expand All @@ -32,12 +33,13 @@ export function set_svg_class(dom, value) {
/**
* @param {MathMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_mathml_class(dom, value) {
export function set_mathml_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
var next_class_name = to_class(value, hash);

if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
Expand All @@ -61,12 +63,13 @@ export function set_mathml_class(dom, value) {
/**
* @param {HTMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_class(dom, value) {
export function set_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value);
var next_class_name = to_class(value, hash);

if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
Expand All @@ -79,7 +82,7 @@ export function set_class(dom, value) {
// Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null) {
if (value == null && !hash) {
dom.removeAttribute('class');
} else {
dom.className = next_class_name;
Expand All @@ -93,10 +96,11 @@ export function set_class(dom, value) {
/**
* @template V
* @param {V} value
* @param {string} [hash]
* @returns {string | V}
*/
function to_class(value) {
return value == null ? '' : value;
function to_class(value, hash) {
return (value == null ? '' : value) + (hash ? ' ' + hash : '');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export {
$window as window,
$document as document
} from './dom/operations.js';
export { attr } from '../shared/attributes.js';
export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js';
export {
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr } from '../shared/attributes.js';
import { attr, clsx } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
Expand Down Expand Up @@ -522,7 +522,7 @@ export function once(get_value) {
};
}

export { attr };
export { attr, clsx };

export { html } from './blocks/html.js';

Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/src/internal/shared/attributes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { escape_html } from '../../escaping.js';
import { clsx as _clsx } from 'clsx';

/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
Expand Down Expand Up @@ -26,3 +27,16 @@ export function attr(name, value, is_boolean = false) {
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}

/**
* Small wrapper around clsx to preserve Svelte's (weird) handling of falsy values.
* TODO Svelte 6 revisit this, and likely turn all falsy values into the empty string (what clsx also does)
* @param {any} value
*/
export function clsx(value) {
if (typeof value === 'object') {
return _clsx(value);
} else {
return value ?? '';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true');

component.testName = {};
assert.equal(div.className, '[object Object]');
assert.equal(div.className, '');

component.testName = '';
assert.equal(div.className, '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');

component.testName = {};
assert.equal(div.className, '[object Object] svelte-x1o6ra');
assert.equal(div.className, ' svelte-x1o6ra');

component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true');

component.testName = {};
assert.equal(div.className, '[object Object]');
assert.equal(div.className, '');

component.testName = '';
assert.equal(div.className, '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');

component.testName = {};
assert.equal(div.className, '[object Object] svelte-x1o6ra');
assert.equal(div.className, ' svelte-x1o6ra');

component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra');
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading