mirror of https://github.com/sveltejs/svelte
Mirrors the clsx-style ergonomics that `class` has had since 5.16. The `style` attribute now accepts an object, array, or any nested combination, normalised by a small runtime helper alongside the existing `to_style`. Object keys are emitted verbatim (no camelCase conversion), falsy entries are dropped, and existing `style:` directive precedence is unchanged.pull/18176/head
parent
dc5bd887b5
commit
e4ad69ea76
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: allow `style` attribute to be an object or array, mirroring the `class` attribute
|
||||
@ -0,0 +1,145 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client', 'hydrate', 'server'],
|
||||
|
||||
async test({ assert, target }) {
|
||||
const get = (/** @type {string} */ id) =>
|
||||
/** @type {HTMLElement} */ (target.querySelector('#' + id));
|
||||
|
||||
// --- inline literal cases ---
|
||||
|
||||
// inline object literal
|
||||
assert.equal(get('inline-object').style.color, 'red');
|
||||
assert.equal(get('inline-object').style.backgroundColor, 'blue');
|
||||
|
||||
// inline array of strings
|
||||
assert.equal(get('inline-array-strings').style.color, 'red');
|
||||
assert.equal(get('inline-array-strings').style.backgroundColor, 'blue');
|
||||
|
||||
// inline array mixing strings and objects (and stray trailing semicolons)
|
||||
assert.equal(get('inline-array-mixed').style.color, 'red');
|
||||
assert.equal(get('inline-array-mixed').style.padding, '4px');
|
||||
assert.equal(get('inline-array-mixed').style.margin, '2px');
|
||||
|
||||
// numeric values, including 0
|
||||
assert.equal(get('numeric').style.zIndex, '0');
|
||||
assert.equal(get('numeric').style.opacity, '1');
|
||||
assert.equal(get('numeric').style.lineHeight, '1.5');
|
||||
|
||||
// nested arrays are flattened
|
||||
assert.equal(get('nested').style.color, 'red');
|
||||
assert.equal(get('nested').style.padding, '4px');
|
||||
assert.equal(get('nested').style.margin, '2px');
|
||||
|
||||
// all entries falsy → attribute absent (or empty)
|
||||
assert.notOk(get('all-falsy').getAttribute('style'));
|
||||
|
||||
// empty object → empty style attribute (parallels clsx({}) === '')
|
||||
const empty_attr = get('empty-object').getAttribute('style');
|
||||
assert.ok(empty_attr === '' || empty_attr === null);
|
||||
|
||||
// conditional inline object: dropped when the gate is false
|
||||
assert.equal(get('conditional').style.padding, '4px');
|
||||
assert.equal(get('conditional').style.color, '');
|
||||
|
||||
// `false`/null/undefined values inside an object are skipped per-property
|
||||
assert.equal(get('falsy-property').style.color, 'red');
|
||||
assert.equal(get('falsy-property').style.backgroundColor, '');
|
||||
assert.equal(get('falsy-property').style.padding, '');
|
||||
assert.equal(get('falsy-property').style.margin, '');
|
||||
|
||||
// reactive cases via direct $state reads: initial render
|
||||
assert.equal(get('reactive-object').style.color, 'red');
|
||||
assert.equal(get('reactive-object').style.backgroundColor, '');
|
||||
assert.equal(get('reactive-array').style.padding, '2px');
|
||||
assert.equal(get('reactive-array').style.color, 'red');
|
||||
assert.equal(get('reactive-array').style.borderColor, '');
|
||||
|
||||
// CSS custom properties are emitted verbatim
|
||||
assert.equal(get('custom-prop').style.getPropertyValue('--my-color'), 'red');
|
||||
assert.equal(get('custom-prop').style.getPropertyValue('--scale'), '1');
|
||||
|
||||
// style: directive wins on overlapping property
|
||||
assert.equal(get('directive-precedence').style.color, 'blue');
|
||||
assert.equal(get('directive-precedence').style.padding, '4px');
|
||||
|
||||
// spread
|
||||
assert.equal(get('spread').style.color, 'red');
|
||||
assert.equal(get('spread').style.padding, '1px');
|
||||
|
||||
// --- $derived cases ---
|
||||
|
||||
// $derived returning an object
|
||||
assert.equal(get('derived-object').style.color, 'red');
|
||||
assert.equal(get('derived-object').style.padding, '2px');
|
||||
|
||||
// $derived returning a mixed array
|
||||
assert.equal(get('derived-array').style.margin, '2px');
|
||||
assert.equal(get('derived-array').style.color, 'red');
|
||||
assert.equal(get('derived-array').style.borderWidth, '2px');
|
||||
|
||||
// $derived returning a string
|
||||
assert.equal(get('derived-string').style.color, 'red');
|
||||
assert.equal(get('derived-string').style.opacity, '1');
|
||||
|
||||
// $derived nested inside an inline array, alongside a literal
|
||||
assert.equal(get('derived-in-array').style.outline, '1px solid red');
|
||||
assert.equal(get('derived-in-array').style.color, 'red');
|
||||
assert.equal(get('derived-in-array').style.padding, '2px');
|
||||
|
||||
// $derived gated by a condition: when falsy, no attribute should be emitted
|
||||
assert.notOk(get('derived-conditional').getAttribute('style'));
|
||||
|
||||
// $derived combined with style: directive — directive wins
|
||||
assert.equal(get('derived-directive').style.color, 'blue');
|
||||
assert.equal(get('derived-directive').style.padding, '2px');
|
||||
|
||||
// $derived inside spread
|
||||
assert.equal(get('derived-spread').style.color, 'red');
|
||||
assert.equal(get('derived-spread').style.padding, '2px');
|
||||
|
||||
// $derived object with conditional falsy values
|
||||
assert.equal(get('derived-falsy').style.color, 'red');
|
||||
assert.equal(get('derived-falsy').style.backgroundColor, '');
|
||||
assert.equal(get('derived-falsy').style.borderColor, 'black');
|
||||
|
||||
// --- reactivity ---
|
||||
|
||||
const button = /** @type {HTMLButtonElement} */ (target.querySelector('button'));
|
||||
button.click();
|
||||
flushSync();
|
||||
|
||||
// inline reactive: $state reads recompute
|
||||
assert.equal(get('reactive-object').style.color, 'green');
|
||||
assert.equal(get('reactive-object').style.backgroundColor, 'yellow');
|
||||
assert.equal(get('reactive-array').style.color, 'green');
|
||||
assert.equal(get('reactive-array').style.borderColor, 'green');
|
||||
assert.equal(get('conditional').style.color, 'green');
|
||||
assert.equal(get('custom-prop').style.getPropertyValue('--my-color'), 'green');
|
||||
assert.equal(get('directive-precedence').style.color, 'blue'); // still wins
|
||||
assert.equal(get('spread').style.color, 'green');
|
||||
|
||||
// $derived cases recompute
|
||||
assert.equal(get('derived-object').style.color, 'green');
|
||||
assert.equal(get('derived-object').style.padding, '8px');
|
||||
assert.equal(get('derived-array').style.color, 'green');
|
||||
assert.equal(get('derived-array').style.borderWidth, '8px');
|
||||
assert.equal(get('derived-string').style.color, 'green');
|
||||
assert.equal(get('derived-string').style.opacity, '0.5');
|
||||
assert.equal(get('derived-in-array').style.color, 'green');
|
||||
assert.equal(get('derived-in-array').style.padding, '8px');
|
||||
// derived-conditional now resolves to a real object
|
||||
assert.equal(get('derived-conditional').style.backgroundColor, 'yellow');
|
||||
// derived-directive: directive still wins, but padding tracks the derived
|
||||
assert.equal(get('derived-directive').style.color, 'blue');
|
||||
assert.equal(get('derived-directive').style.padding, '8px');
|
||||
assert.equal(get('derived-spread').style.color, 'green');
|
||||
assert.equal(get('derived-spread').style.padding, '8px');
|
||||
// derived-falsy: now `background-color` shows up and `border-color` drops
|
||||
assert.equal(get('derived-falsy').style.color, 'green');
|
||||
assert.equal(get('derived-falsy').style.backgroundColor, 'yellow');
|
||||
assert.equal(get('derived-falsy').style.borderColor, '');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,82 @@
|
||||
<script>
|
||||
let active = $state(false);
|
||||
let color = $state('red');
|
||||
let size = $state(2);
|
||||
|
||||
let derived_object = $derived({ color, padding: size + 'px' });
|
||||
let derived_array = $derived(['margin: 2px', { color, 'border-width': size + 'px' }]);
|
||||
let derived_string = $derived(`color: ${color}; opacity: ${active ? 0.5 : 1};`);
|
||||
let derived_conditional = $derived(active && { 'background-color': 'yellow' });
|
||||
let derived_spread = $derived({ style: { color, padding: size + 'px' } });
|
||||
let derived_with_falsy = $derived({
|
||||
color,
|
||||
'background-color': active ? 'yellow' : false,
|
||||
'border-color': active ? null : 'black'
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- inline object literal -->
|
||||
<div id="inline-object" style={{ color: 'red', 'background-color': 'blue' }}></div>
|
||||
|
||||
<!-- inline array of strings -->
|
||||
<div id="inline-array-strings" style={['color: red', 'background-color: blue']}></div>
|
||||
|
||||
<!-- inline array mixing strings and objects, with trailing semicolons in the strings -->
|
||||
<div id="inline-array-mixed" style={['color: red;', { padding: '4px', margin: '2px' }]}></div>
|
||||
|
||||
<!-- numeric values are kept as-is (including zero) -->
|
||||
<div id="numeric" style={{ 'z-index': 0, opacity: 1, 'line-height': 1.5 }}></div>
|
||||
|
||||
<!-- nested arrays are flattened -->
|
||||
<div id="nested" style={[['color: red'], [{ padding: '4px' }, ['margin: 2px']]]}></div>
|
||||
|
||||
<!-- every entry is falsy → no style attribute -->
|
||||
<div id="all-falsy" style={[false, null, undefined, '']}></div>
|
||||
|
||||
<!-- empty object → empty style attribute (matches clsx({}) = '') -->
|
||||
<div id="empty-object" style={{}}></div>
|
||||
|
||||
<!-- conditional inline object: when the condition is false the whole object is dropped -->
|
||||
<div id="conditional" style={[{ padding: '4px' }, active && { color: 'green' }]}></div>
|
||||
|
||||
<!-- object value that is `false` is filtered out at the property level -->
|
||||
<div id="falsy-property" style={{ color: 'red', 'background-color': false, padding: null, margin: undefined }}></div>
|
||||
|
||||
<!-- inline reactive: rebuilt each render via direct $state reads -->
|
||||
<div id="reactive-object" style={{ color, 'background-color': active && 'yellow' }}></div>
|
||||
<div id="reactive-array" style={['padding: 2px', { color, 'border-color': active && 'green' }]}></div>
|
||||
|
||||
<!-- CSS custom properties are emitted verbatim -->
|
||||
<div id="custom-prop" style={{ '--my-color': color, '--scale': 1 }}></div>
|
||||
|
||||
<!-- style: directive wins on overlapping property -->
|
||||
<div id="directive-precedence" style={{ color: 'red', padding: '4px' }} style:color="blue"></div>
|
||||
|
||||
<!-- spread carrying an object -->
|
||||
<div id="spread" {...{ style: { color, padding: '1px' } }}></div>
|
||||
|
||||
<!-- $derived returning an object -->
|
||||
<div id="derived-object" style={derived_object}></div>
|
||||
|
||||
<!-- $derived returning a mixed array -->
|
||||
<div id="derived-array" style={derived_array}></div>
|
||||
|
||||
<!-- $derived returning a string (existing behaviour, exercised through the same path) -->
|
||||
<div id="derived-string" style={derived_string}></div>
|
||||
|
||||
<!-- $derived nested inside an inline array, alongside a literal -->
|
||||
<div id="derived-in-array" style={['outline: 1px solid red', derived_object]}></div>
|
||||
|
||||
<!-- $derived gated by a condition: when falsy, no attribute should be emitted -->
|
||||
<div id="derived-conditional" style={derived_conditional}></div>
|
||||
|
||||
<!-- $derived combined with style: directive — directive must win -->
|
||||
<div id="derived-directive" style={derived_object} style:color="blue"></div>
|
||||
|
||||
<!-- $derived inside spread -->
|
||||
<div id="derived-spread" {...derived_spread}></div>
|
||||
|
||||
<!-- $derived object with conditional falsy values -->
|
||||
<div id="derived-falsy" style={derived_with_falsy}></div>
|
||||
|
||||
<button onclick={() => { active = !active; color = active ? 'green' : 'red'; size = active ? 8 : 2; }}>toggle</button>
|
||||
Loading…
Reference in new issue