Skip to content

Commit 1cd9011

Browse files
committed
allow objects/arrays for style attribute
1 parent 32b78c9 commit 1cd9011

File tree

10 files changed

+104
-10
lines changed

10 files changed

+104
-10
lines changed

packages/svelte/elements.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
764764
placeholder?: string | undefined | null;
765765
slot?: string | undefined | null;
766766
spellcheck?: Booleanish | undefined | null;
767-
style?: string | undefined | null;
767+
style?: StyleValue | undefined | null;
768768
tabindex?: number | undefined | null;
769769
title?: string | undefined | null;
770770
translate?: 'yes' | 'no' | '' | undefined | null;
@@ -2062,3 +2062,7 @@ export interface SvelteHTMLElements {
20622062
}
20632063

20642064
export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;
2065+
2066+
type StyleDictionary = Record<string, any>;
2067+
type StyleArray = StyleValue[];
2068+
export type StyleValue = StyleArray | StyleDictionary | string | null | undefined;

packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ export function Attribute(node, context) {
4646
node.metadata.needs_clsx = true;
4747
}
4848

49+
// style={[...]} or style={{...}} or `style={x}` need cssx to resolve the style
50+
if (
51+
node.name === 'style' &&
52+
!Array.isArray(node.value) &&
53+
node.value !== true &&
54+
node.value.expression.type !== 'Literal' &&
55+
node.value.expression.type !== 'TemplateLiteral' &&
56+
node.value.expression.type !== 'BinaryExpression'
57+
) {
58+
// TODO ??? mark_subtree_dynamic(context.path);
59+
node.metadata.needs_cssx = true;
60+
}
61+
4962
if (node.value !== true) {
5063
for (const chunk of get_attribute_chunks(node.value)) {
5164
if (chunk.type !== 'ExpressionTag') continue;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ function build_element_attribute_update_assignment(
594594
} else if (is_dom_property(name)) {
595595
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
596596
} else {
597+
if (attribute.metadata.needs_cssx) {
598+
value = b.call('$.cssx', value);
599+
}
600+
597601
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
598602
update = b.stmt(
599603
b.call(
@@ -635,6 +639,11 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
635639
value = b.call('$.clsx', value);
636640
}
637641

642+
// We assume that noone's going to redefine the semantics of the style attribute on custom elements, i.e. it's still used for CSS styles
643+
if (name === 'style' && attribute.metadata.needs_cssx) {
644+
value = b.call('$.cssx', value);
645+
}
646+
638647
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
639648

640649
if (has_state) {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,25 @@ export function build_element_attributes(node, context) {
108108
} else {
109109
attributes.push(attribute);
110110
}
111-
} else {
112-
if (attribute.name === 'style') {
113-
style_index = attributes.length;
114-
}
111+
} else if (attribute.name === 'style') {
112+
style_index = attributes.length;
115113

114+
if (attribute.metadata.needs_cssx) {
115+
const clsx_value = b.call(
116+
'$.cssx',
117+
/** @type {AST.ExpressionTag} */ (attribute.value).expression
118+
);
119+
attributes.push({
120+
...attribute,
121+
value: {
122+
.../** @type {AST.ExpressionTag} */ (attribute.value),
123+
expression: clsx_value
124+
}
125+
});
126+
} else {
127+
attributes.push(attribute);
128+
}
129+
} else {
116130
attributes.push(attribute);
117131
}
118132
}

packages/svelte/src/compiler/phases/nodes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export function create_attribute(name, start, end, value) {
4949
value,
5050
metadata: {
5151
delegated: null,
52-
needs_clsx: false
52+
needs_clsx: false,
53+
needs_cssx: false
5354
}
5455
};
5556
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,8 @@ export namespace AST {
481481
delegated: null | DelegatedEvent;
482482
/** May be `true` if this is a `class` attribute that needs `clsx` */
483483
needs_clsx: boolean;
484+
/** May be `true` if this is a `style` attribute that needs `cssx` */
485+
needs_cssx: boolean;
484486
};
485487
}
486488

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
set_active_effect,
1414
set_active_reaction
1515
} from '../../runtime.js';
16-
import { clsx } from '../../../shared/attributes.js';
16+
import { clsx, cssx } from '../../../shared/attributes.js';
1717

1818
/**
1919
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@@ -291,6 +291,10 @@ export function set_attributes(
291291
next.class = clsx(next.class);
292292
}
293293

294+
if (next.style) {
295+
next.style = cssx(next.style);
296+
}
297+
294298
if (css_hash !== undefined) {
295299
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
296300
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export {
155155
$window as window,
156156
$document as document
157157
} from './dom/operations.js';
158-
export { attr, clsx } from '../shared/attributes.js';
158+
export { attr, clsx, cssx } from '../shared/attributes.js';
159159
export { snapshot } from '../shared/clone.js';
160160
export { noop, fallback } from '../shared/utils.js';
161161
export {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { Component, Payload, RenderOutput } from '#server' */
33
/** @import { Store } from '#shared' */
44
export { FILENAME, HMR } from '../../constants.js';
5-
import { attr, clsx } from '../shared/attributes.js';
5+
import { attr, clsx, cssx } from '../shared/attributes.js';
66
import { is_promise, noop } from '../shared/utils.js';
77
import { subscribe_to_store } from '../../store/utils.js';
88
import {
@@ -204,6 +204,10 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
204204
* @returns {string}
205205
*/
206206
export function spread_attributes(attrs, classes, styles, flags = 0) {
207+
if (attrs.style) {
208+
attrs.style = cssx(attrs.style);
209+
}
210+
207211
if (styles) {
208212
attrs.style = attrs.style
209213
? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
@@ -552,7 +556,7 @@ export function props_id(payload) {
552556
return uid;
553557
}
554558

555-
export { attr, clsx };
559+
export { attr, clsx, cssx };
556560

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

packages/svelte/src/internal/shared/attributes.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,46 @@ export function clsx(value) {
4040
return value ?? '';
4141
}
4242
}
43+
44+
/**
45+
* Format a CSS key/value
46+
* @param {[string,any]} value
47+
* @returns {string|null}
48+
*/
49+
function cssx_format([k, v]) {
50+
if (v == null) {
51+
return null;
52+
}
53+
v = ('' + v).trim();
54+
if (v === '') {
55+
return null;
56+
}
57+
if (k[0] !== '-' && k[1] !== '-') {
58+
k = k
59+
.replaceAll('_', '-')
60+
.replaceAll(/(?<=[a-z])[A-Z](?=[a-z])/g, (c) => '-' + c)
61+
.toLowerCase();
62+
}
63+
return k + ':' + v;
64+
}
65+
66+
/**
67+
* Build a style attributes based on arrays/objects/strings
68+
* @param {any} value
69+
* @returns {string|null}
70+
*/
71+
export function cssx(value) {
72+
if (value == null) {
73+
return null;
74+
}
75+
if (typeof value === 'object') {
76+
if (value instanceof CSSStyleDeclaration) {
77+
// Special case for CSSStyleDeclaration
78+
return value.cssText;
79+
}
80+
return (Array.isArray(value) ? value.map(cssx) : Object.entries(value).map(cssx_format))
81+
.filter((v) => v)
82+
.join(';');
83+
}
84+
return value;
85+
}

0 commit comments

Comments
 (0)