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';
|
import { hydrating } from '../hydration.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SVGElement} dom
|
* @param {Element} dom
|
||||||
* @param {string} value
|
* @param {boolean | number} is_html
|
||||||
* @param {string} [hash]
|
* @param {string | null} value
|
||||||
* @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 {string} [hash]
|
* @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
|
// @ts-expect-error need to add __className to patched prototype
|
||||||
var prev_class_name = dom.__className;
|
var prev = dom.__className;
|
||||||
var next_class_name = to_class(value, hash);
|
|
||||||
|
if (hydrating || prev !== value) {
|
||||||
if (hydrating && dom.getAttribute('class') === next_class_name) {
|
var next_class_name = to_class(value, hash, next_classes);
|
||||||
// In case of hydration don't reset the class as it's already correct.
|
|
||||||
// @ts-expect-error need to add __className to patched prototype
|
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
|
||||||
dom.__className = next_class_name;
|
// Removing the attribute when the value is only an empty string causes
|
||||||
} else if (
|
// performance issues vs simply making the className an empty string. So
|
||||||
prev_class_name !== next_class_name ||
|
// we should only remove the class if the the value is nullish
|
||||||
(hydrating && dom.getAttribute('class') !== next_class_name)
|
// and there no hash/directives :
|
||||||
) {
|
if (next_class_name == null) {
|
||||||
if (next_class_name === '') {
|
dom.removeAttribute('class');
|
||||||
dom.removeAttribute('class');
|
} else if (is_html) {
|
||||||
} else {
|
dom.className = next_class_name;
|
||||||
dom.setAttribute('class', next_class_name);
|
} else {
|
||||||
|
dom.setAttribute('class', next_class_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error need to add __className to patched prototype
|
// @ts-expect-error need to add __className to patched prototype
|
||||||
dom.__className = next_class_name;
|
dom.__className = value;
|
||||||
}
|
} else if (next_classes) {
|
||||||
}
|
prev_classes ??= {};
|
||||||
|
|
||||||
/**
|
for (var key in next_classes) {
|
||||||
* @param {HTMLElement} dom
|
var is_present = !!next_classes[key];
|
||||||
* @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);
|
|
||||||
|
|
||||||
if (hydrating && dom.className === next_class_name) {
|
if (is_present !== !!prev_classes[key]) {
|
||||||
// In case of hydration don't reset the class as it's already correct.
|
dom.classList.toggle(key, is_present);
|
||||||
// @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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error need to add __className to patched prototype
|
|
||||||
dom.__className = next_class_name;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return next_classes;
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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