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 {Element & ElementCSSInlineStyle} dom
|
||||||
* @param {string} key
|
* @param {Record<string, any>} prev
|
||||||
* @param {string} value
|
* @param {Record<string, any>} next
|
||||||
* @param {boolean} [important]
|
* @param {string} [priority]
|
||||||
*/
|
*/
|
||||||
export function set_style(dom, key, value, important) {
|
function update_styles(dom, prev = {}, next, priority) {
|
||||||
// @ts-expect-error
|
for (var key in next) {
|
||||||
var styles = (dom.__styles ??= {});
|
var value = next[key];
|
||||||
|
|
||||||
if (styles[key] === value) {
|
if (prev[key] !== value) {
|
||||||
return;
|
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 (hydrating || prev !== value) {
|
||||||
|
var next_style_attr = to_style(value, next_styles);
|
||||||
|
|
||||||
if (value == null) {
|
if (!hydrating || next_style_attr !== dom.getAttribute('style')) {
|
||||||
dom.style.removeProperty(key);
|
if (next_style_attr == null) {
|
||||||
} else {
|
dom.removeAttribute('style');
|
||||||
dom.style.setProperty(key, value, important ? 'important' : '');
|
} else {
|
||||||
|
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>
|
<script>
|
||||||
export let styles = `color: red`;
|
export let styles = `color: red;`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p style="opacity: 0.5; {styles}">{styles}</p>
|
<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