mirror of https://github.com/sveltejs/svelte
chore: rewrite set_class() to handle directives (#15352)
* set_class with class: directives * update expected result (remove leading space) * fix * optimize literals * add test * add test for mutations on hydration * clean observer * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js unused Co-authored-by: Rich Harris <hello@rich-harris.dev> * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js unused for now Co-authored-by: Rich Harris <hello@rich-harris.dev> * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js unused for now Co-authored-by: Rich Harris <hello@rich-harris.dev> * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js nit Co-authored-by: Rich Harris <hello@rich-harris.dev> * Update packages/svelte/src/internal/client/dom/elements/attributes.js nit Co-authored-by: Rich Harris <hello@rich-harris.dev> * Update packages/svelte/src/internal/shared/attributes.js rename clazz to value :D Co-authored-by: Rich Harris <hello@rich-harris.dev> * remove unused + fix JSDoc * drive-by fix * minor style tweaks * tweak test * this is faster * tweak * tweak * this is faster * typo * tweak * changeset --------- Co-authored-by: Rich Harris <hello@rich-harris.dev> Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/15358/head
parent
5a946e7905
commit
d4360af751
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: correctly override class attributes with class directives
|
@ -1,120 +1,49 @@
|
||||
import { to_class } from '../../../shared/attributes.js';
|
||||
import { hydrating } from '../hydration.js';
|
||||
|
||||
/**
|
||||
* @param {SVGElement} dom
|
||||
* @param {string} value
|
||||
* @param {string} [hash]
|
||||
* @returns {void}
|
||||
*/
|
||||
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, hash);
|
||||
|
||||
if (hydrating && dom.getAttribute('class') === next_class_name) {
|
||||
// In case of hydration don't reset the class as it's already correct.
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
} else if (
|
||||
prev_class_name !== next_class_name ||
|
||||
(hydrating && dom.getAttribute('class') !== next_class_name)
|
||||
) {
|
||||
if (next_class_name === '') {
|
||||
dom.removeAttribute('class');
|
||||
} else {
|
||||
dom.setAttribute('class', next_class_name);
|
||||
}
|
||||
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MathMLElement} dom
|
||||
* @param {string} value
|
||||
* @param {Element} dom
|
||||
* @param {boolean | number} is_html
|
||||
* @param {string | null} value
|
||||
* @param {string} [hash]
|
||||
* @returns {void}
|
||||
* @param {Record<string, boolean>} [prev_classes]
|
||||
* @param {Record<string, boolean>} [next_classes]
|
||||
* @returns {Record<string, boolean> | undefined}
|
||||
*/
|
||||
export function set_mathml_class(dom, value, hash) {
|
||||
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
var prev_class_name = dom.__className;
|
||||
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.
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
} else if (
|
||||
prev_class_name !== next_class_name ||
|
||||
(hydrating && dom.getAttribute('class') !== next_class_name)
|
||||
) {
|
||||
if (next_class_name === '') {
|
||||
dom.removeAttribute('class');
|
||||
} else {
|
||||
dom.setAttribute('class', next_class_name);
|
||||
var prev = dom.__className;
|
||||
|
||||
if (hydrating || prev !== value) {
|
||||
var next_class_name = to_class(value, hash, next_classes);
|
||||
|
||||
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
|
||||
// Removing the attribute when the value is only an empty string causes
|
||||
// performance issues vs simply making the className an empty string. So
|
||||
// we should only remove the class if the the value is nullish
|
||||
// and there no hash/directives :
|
||||
if (next_class_name == null) {
|
||||
dom.removeAttribute('class');
|
||||
} else if (is_html) {
|
||||
dom.className = next_class_name;
|
||||
} else {
|
||||
dom.setAttribute('class', next_class_name);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
}
|
||||
}
|
||||
dom.__className = value;
|
||||
} else if (next_classes) {
|
||||
prev_classes ??= {};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} dom
|
||||
* @param {string} value
|
||||
* @param {string} [hash]
|
||||
* @returns {void}
|
||||
*/
|
||||
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, hash);
|
||||
for (var key in next_classes) {
|
||||
var is_present = !!next_classes[key];
|
||||
|
||||
if (hydrating && dom.className === next_class_name) {
|
||||
// In case of hydration don't reset the class as it's already correct.
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
} else if (
|
||||
prev_class_name !== next_class_name ||
|
||||
(hydrating && dom.className !== next_class_name)
|
||||
) {
|
||||
// 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 && !hash) {
|
||||
dom.removeAttribute('class');
|
||||
} else {
|
||||
dom.className = next_class_name;
|
||||
if (is_present !== !!prev_classes[key]) {
|
||||
dom.classList.toggle(key, is_present);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error need to add __className to patched prototype
|
||||
dom.__className = next_class_name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template V
|
||||
* @param {V} value
|
||||
* @param {string} [hash]
|
||||
* @returns {string | V}
|
||||
*/
|
||||
function to_class(value, hash) {
|
||||
return (value == null ? '' : value) + (hash ? ' ' + hash : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} dom
|
||||
* @param {string} class_name
|
||||
* @param {boolean} value
|
||||
* @returns {void}
|
||||
*/
|
||||
export function toggle_class(dom, class_name, value) {
|
||||
if (value) {
|
||||
if (dom.classList.contains(class_name)) return;
|
||||
dom.classList.add(class_name);
|
||||
} else {
|
||||
if (!dom.classList.contains(class_name)) return;
|
||||
dom.classList.remove(class_name);
|
||||
}
|
||||
return next_classes;
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
<p class=" svelte-xyz">Foo</p>
|
||||
<p class="svelte-xyz">Foo</p>
|
@ -0,0 +1,43 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
// This test counts mutations on hydration
|
||||
// set_class() should not mutate class on hydration, except if mismatch
|
||||
export default test({
|
||||
mode: ['server', 'hydrate'],
|
||||
|
||||
server_props: {
|
||||
browser: false
|
||||
},
|
||||
|
||||
props: {
|
||||
browser: true
|
||||
},
|
||||
|
||||
html: `
|
||||
<main id="main" class="browser">
|
||||
<div class="custom svelte-1cjqok6 foo bar"></div>
|
||||
<span class="svelte-1cjqok6 foo bar"></span>
|
||||
<b class="custom foo bar"></b>
|
||||
<i class="foo bar"></i>
|
||||
</main>
|
||||
`,
|
||||
|
||||
ssrHtml: `
|
||||
<main id="main">
|
||||
<div class="custom svelte-1cjqok6 foo bar"></div>
|
||||
<span class="svelte-1cjqok6 foo bar"></span>
|
||||
<b class="custom foo bar"></b>
|
||||
<i class="foo bar"></i>
|
||||
</main>
|
||||
`,
|
||||
|
||||
async test({ assert, component, instance }) {
|
||||
flushSync();
|
||||
assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
|
||||
|
||||
component.foo = false;
|
||||
flushSync();
|
||||
assert.deepEqual(instance.get_and_clear_mutations(), ['DIV', 'SPAN', 'B', 'I']);
|
||||
}
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
<script>
|
||||
let {
|
||||
classname = 'custom',
|
||||
foo = true,
|
||||
bar = true,
|
||||
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" class:browser>
|
||||
<div class={classname} class:foo class:bar></div>
|
||||
<span class:foo class:bar></span>
|
||||
<b class={classname} class:foo class:bar></b>
|
||||
<i class:foo class:bar></i>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
div,
|
||||
span {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,145 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
|
||||
<div class="foo svelte-tza1s0 bar"></div>
|
||||
<span class="foo bar"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0 bar"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 bar"></div>
|
||||
<span class="bar"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar"></span></div>
|
||||
|
||||
<div class="football svelte-tza1s0 bar"></div>
|
||||
<span class="football bar"></span>
|
||||
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 bar"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 bar not-foo"></div>
|
||||
<span class="bar not-foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar not-foo"></span></div>
|
||||
|
||||
`,
|
||||
test({ assert, target, component }) {
|
||||
component.foo = true;
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0 bar"></div>
|
||||
<span class="foo bar"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0 bar"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 foo"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 bar foo"></div>
|
||||
<span class="bar foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar foo"></span></div>
|
||||
|
||||
<div class="football svelte-tza1s0 bar foo"></div>
|
||||
<span class="football bar foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 bar foo"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 bar foo"></div>
|
||||
<span class="bar foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar foo"></span></div>
|
||||
`
|
||||
);
|
||||
|
||||
component.bar = false;
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 foo"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 foo"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
|
||||
|
||||
<div class="football svelte-tza1s0 foo"></div>
|
||||
<span class="football foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 foo"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 foo"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
|
||||
`
|
||||
);
|
||||
|
||||
component.foo = false;
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="foo svelte-tza1s0"></div>
|
||||
<span class="foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span class=""></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0"></div>
|
||||
<span class=""></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="football svelte-tza1s0"></div>
|
||||
<span class="football"></span>
|
||||
<div class="svelte-tza1s0"><span class="football svelte-tza1s0"></span></div>
|
||||
|
||||
<div class="svelte-tza1s0 not-foo"></div>
|
||||
<span class="not-foo"></span>
|
||||
<div class="svelte-tza1s0"><span class="svelte-tza1s0 not-foo"></span></div>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
<script>
|
||||
let { foo = false, bar = true } = $props();
|
||||
</script>
|
||||
|
||||
<div></div>
|
||||
<span></span>
|
||||
<div><span></span></div>
|
||||
|
||||
<div class="foo"></div>
|
||||
<span class="foo"></span>
|
||||
<div><span class="foo"></span></div>
|
||||
|
||||
<div class="foo" class:bar></div>
|
||||
<span class="foo" class:bar></span>
|
||||
<div><span class="foo" class:bar></span></div>
|
||||
|
||||
<div class="foo" class:foo></div>
|
||||
<span class="foo" class:foo></span>
|
||||
<div><span class="foo" class:foo></span></div>
|
||||
|
||||
<div class="foo" class:bar class:foo></div>
|
||||
<span class="foo" class:bar class:foo></span>
|
||||
<div><span class="foo" class:bar class:foo></span></div>
|
||||
|
||||
<div class="football" class:bar class:foo></div>
|
||||
<span class="football" class:bar class:foo></span>
|
||||
<div><span class="football" class:bar class:foo></span></div>
|
||||
|
||||
<div class="foo" class:bar class:foo class:not-foo={!foo}></div>
|
||||
<span class="foo" class:bar class:foo class:not-foo={!foo}></span>
|
||||
<div><span class="foo" class:bar class:foo class:not-foo={!foo}></span></div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
color: red;
|
||||
}
|
||||
div > span {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
Loading…
Reference in new issue