mirror of https://github.com/sveltejs/svelte
chore: rewrite set_style() to handle directives (#15418)
* add style attribute when needed * set_style() * to_style() * remove `style=""` * use cssTest for perfs * base test * test * changeset * revert dom.style.cssText * format name * use style.cssText + adapt test * Apply suggestions from code review suggestions from dummdidumm Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * fix priority * lint * yawn * update test * we can simplify some stuff now * simplify * more * simplify some more * more * more * more * more * more * remove continue * tweak * tweak * tweak * skip hash argument where possible * tweak * tweak * tweak * tweak --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/15453/head
parent
2f685c1dba
commit
30562b8780
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: make `style:` directive and CSS handling more robust
|
@ -1,22 +1,57 @@
|
||||
import { to_style } from '../../../shared/attributes.js';
|
||||
import { hydrating } from '../hydration.js';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dom
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
* @param {boolean} [important]
|
||||
* @param {Element & ElementCSSInlineStyle} dom
|
||||
* @param {Record<string, any>} prev
|
||||
* @param {Record<string, any>} next
|
||||
* @param {string} [priority]
|
||||
*/
|
||||
export function set_style(dom, key, value, important) {
|
||||
// @ts-expect-error
|
||||
var styles = (dom.__styles ??= {});
|
||||
function update_styles(dom, prev = {}, next, priority) {
|
||||
for (var key in next) {
|
||||
var value = next[key];
|
||||
|
||||
if (styles[key] === value) {
|
||||
return;
|
||||
if (prev[key] !== value) {
|
||||
if (next[key] == null) {
|
||||
dom.style.removeProperty(key);
|
||||
} else {
|
||||
dom.style.setProperty(key, value, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styles[key] = value;
|
||||
/**
|
||||
* @param {Element & ElementCSSInlineStyle} dom
|
||||
* @param {string | null} value
|
||||
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [prev_styles]
|
||||
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
|
||||
*/
|
||||
export function set_style(dom, value, prev_styles, next_styles) {
|
||||
// @ts-expect-error
|
||||
var prev = dom.__style;
|
||||
|
||||
if (value == null) {
|
||||
dom.style.removeProperty(key);
|
||||
if (hydrating || prev !== value) {
|
||||
var next_style_attr = to_style(value, next_styles);
|
||||
|
||||
if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
|
||||
if (next_style_attr == null) {
|
||||
dom.removeAttribute('style');
|
||||
} else {
|
||||
dom.style.setProperty(key, value, important ? 'important' : '');
|
||||
dom.style.cssText = next_style_attr;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
dom.__style = value;
|
||||
} else if (next_styles) {
|
||||
if (Array.isArray(next_styles)) {
|
||||
update_styles(dom, prev_styles?.[0], next_styles[0]);
|
||||
update_styles(dom, prev_styles?.[1], next_styles[1], 'important');
|
||||
} else {
|
||||
update_styles(dom, prev_styles, next_styles);
|
||||
}
|
||||
}
|
||||
|
||||
return next_styles;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
export let styles = `color: red`;
|
||||
export let styles = `color: red;`;
|
||||
</script>
|
||||
|
||||
<p style="opacity: 0.5; {styles}">{styles}</p>
|
@ -0,0 +1,95 @@
|
||||
import { flushSync, tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// This test counts mutations on hydration
|
||||
// set_style() should not mutate style on hydration, except if mismatch
|
||||
export default test({
|
||||
mode: ['server', 'hydrate'],
|
||||
|
||||
server_props: {
|
||||
browser: false
|
||||
},
|
||||
|
||||
props: {
|
||||
browser: true
|
||||
},
|
||||
|
||||
ssrHtml: `
|
||||
<main id="main" style="color: black;">
|
||||
<div style="color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="background:blue; background: linear-gradient(0, white 0%, red 100%); color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url(https://placehold.co/100x100?text=;&font=roboto); color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url("https://placehold.co/100x100?text=;&font=roboto"); color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url('https://placehold.co/100x100?text=;&font=roboto'); color: red; font-size: 18px !important;"></div>
|
||||
</main>
|
||||
`,
|
||||
|
||||
html: `
|
||||
<main id="main" style="color: white;">
|
||||
<div style="color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="background:blue; background: linear-gradient(0, white 0%, red 100%); color: red; font-size: 18px !important;"></div>
|
||||
<div style="border: 1px solid; color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url(https://placehold.co/100x100?text=;&font=roboto); color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url("https://placehold.co/100x100?text=;&font=roboto"); color: red; font-size: 18px !important;"></div>
|
||||
<div style="background: url('https://placehold.co/100x100?text=;&font=roboto'); color: red; font-size: 18px !important;"></div>
|
||||
</main>
|
||||
`,
|
||||
|
||||
async test({ target, assert, component, instance }) {
|
||||
flushSync();
|
||||
tick();
|
||||
assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
|
||||
|
||||
let divs = target.querySelectorAll('div');
|
||||
|
||||
// Note : we cannot compare HTML because set_style() use dom.style.cssText
|
||||
// which can alter the format of the attribute...
|
||||
|
||||
divs.forEach((d) => assert.equal(d.style.margin, ''));
|
||||
divs.forEach((d) => assert.equal(d.style.color, 'red'));
|
||||
divs.forEach((d) => assert.equal(d.style.fontSize, '18px'));
|
||||
|
||||
component.margin = '1px';
|
||||
flushSync();
|
||||
assert.deepEqual(
|
||||
instance.get_and_clear_mutations(),
|
||||
['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
|
||||
'margin'
|
||||
);
|
||||
divs.forEach((d) => assert.equal(d.style.margin, '1px'));
|
||||
|
||||
component.color = 'yellow';
|
||||
flushSync();
|
||||
assert.deepEqual(
|
||||
instance.get_and_clear_mutations(),
|
||||
['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
|
||||
'color'
|
||||
);
|
||||
divs.forEach((d) => assert.equal(d.style.color, 'yellow'));
|
||||
|
||||
component.fontSize = '10px';
|
||||
flushSync();
|
||||
assert.deepEqual(
|
||||
instance.get_and_clear_mutations(),
|
||||
['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
|
||||
'fontSize'
|
||||
);
|
||||
divs.forEach((d) => assert.equal(d.style.fontSize, '10px'));
|
||||
|
||||
component.fontSize = null;
|
||||
flushSync();
|
||||
assert.deepEqual(
|
||||
instance.get_and_clear_mutations(),
|
||||
['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'],
|
||||
'fontSize'
|
||||
);
|
||||
divs.forEach((d) => assert.equal(d.style.fontSize, ''));
|
||||
}
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
<script>
|
||||
let {
|
||||
margin = null,
|
||||
color = 'red',
|
||||
fontSize = '18px',
|
||||
style1 = 'border: 1px solid',
|
||||
style2 = 'border: 1px solid; margin: 1em',
|
||||
style3 = 'color:blue; border: 1px solid; color: green;',
|
||||
style4 = 'background:blue; background: linear-gradient(0, white 0%, red 100%)',
|
||||
style5 = 'border: 1px solid; /* width: 100px; height: 100%; color: green */',
|
||||
style6 = 'background: url(https://placehold.co/100x100?text=;&font=roboto);',
|
||||
style7 = 'background: url("https://placehold.co/100x100?text=;&font=roboto");',
|
||||
style8 = "background: url('https://placehold.co/100x100?text=;&font=roboto');",
|
||||
|
||||
browser
|
||||
} = $props();
|
||||
|
||||
let mutations = [];
|
||||
let observer;
|
||||
|
||||
if (browser) {
|
||||
observer = new MutationObserver(update_mutation_records);
|
||||
observer.observe(document.querySelector('#main'), { attributes: true, subtree: true });
|
||||
|
||||
$effect(() => {
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
function update_mutation_records(results) {
|
||||
for (const r of results) {
|
||||
mutations.push(r.target.nodeName);
|
||||
}
|
||||
}
|
||||
|
||||
export function get_and_clear_mutations() {
|
||||
update_mutation_records(observer.takeRecords());
|
||||
const result = mutations;
|
||||
mutations = [];
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<main id="main" style:color={browser?'white':'black'}>
|
||||
<div style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style1} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style2} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style3} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style4} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style5} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style6} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style7} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
<div style={style8} style:margin style:color style:font-size|important={fontSize}></div>
|
||||
</main>
|
Loading…
Reference in new issue