robustify for multiple blocks:

- revert to previous value on unmount, if it was the last one changing the value
- special handling for classes: merge them
svelte-html
Simon Holthausen 3 weeks ago
parent 0c1c8b992b
commit 86342319d8

@ -1,8 +1,7 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { ExpressionStatement, Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_dom_property, normalize_attribute } from '../../../../../utils.js';
import { is_ignored } from '../../../../state.js';
import { normalize_attribute } from '../../../../../utils.js';
import { is_event_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_attribute_value } from './shared/element.js';
@ -13,7 +12,8 @@ import { visit_event_attribute } from './shared/events.js';
* @param {ComponentContext} context
*/
export function SvelteHTML(element, context) {
const node_id = b.id('$.document.documentElement');
/** @type {Property[]} */
const attributes = [];
for (const attribute of element.attributes) {
if (attribute.type === 'Attribute') {
@ -21,32 +21,9 @@ export function SvelteHTML(element, context) {
visit_event_attribute(attribute, context);
} else {
const name = normalize_attribute(attribute.name);
const { value, has_state } = build_attribute_value(attribute.value, context);
const { value } = build_attribute_value(attribute.value, context);
/** @type {ExpressionStatement} */
let update;
if (name === 'class') {
update = b.stmt(b.call('$.set_class', node_id, value));
} else if (is_dom_property(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else {
update = b.stmt(
b.call(
'$.set_attribute',
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
}
if (has_state) {
context.state.update.push(update);
} else {
context.state.init.push(update);
}
attributes.push(b.init(name, value));
if (context.state.options.dev) {
context.state.init.push(
@ -56,4 +33,8 @@ export function SvelteHTML(element, context) {
}
}
}
if (attributes.length > 0) {
context.state.init.push(b.stmt(b.call('$.svelte_html', b.arrow([], b.object(attributes)))));
}
}

@ -0,0 +1,54 @@
import { render_effect, teardown } from '../../reactivity/effects.js';
import { set_attribute } from '../elements/attributes.js';
import { set_class } from '../elements/class.js';
import { hydrating } from '../hydration.js';
/**
* @param {() => Record<string, any>} get_attributes
* @returns {void}
*/
export function svelte_html(get_attributes) {
const node = document.documentElement;
const own = {};
/** @type {Record<string, Array<[any, any]>>} to check who set the last value of each attribute */
// @ts-expect-error
const current_setters = (node.__attributes_setters ??= {});
/** @type {Record<string, any>} */
let attributes;
render_effect(() => {
attributes = get_attributes();
for (const name in attributes) {
let value = attributes[name];
current_setters[name] = (current_setters[name] ?? []).filter(([owner]) => owner !== own);
current_setters[name].unshift([own, value]);
// Do nothing on initial render during hydration: If there are attribute duplicates, the last value
// wins, which could result in needless hydration repairs from earlier values.
if (hydrating) continue;
if (name === 'class') {
set_class(node, current_setters[name].map(([_, value]) => value).join(' '));
} else {
set_attribute(node, name, value);
}
}
});
teardown(() => {
for (const name in attributes) {
const old = current_setters[name];
current_setters[name] = old.filter(([owner]) => owner !== own);
const current = current_setters[name];
if (name === 'class') {
set_class(node, current.map(([_, value]) => value).join(' '));
} else if (old[0][0] === own) {
set_attribute(node, name, current[0]?.[1]);
}
}
});
}

@ -24,6 +24,7 @@ export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';
export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { svelte_html } from './dom/blocks/svelte-html.js';
export { append_styles } from './dom/css.js';
export { action } from './dom/elements/actions.js';
export {
@ -149,7 +150,12 @@ export {
setContext,
hasContext
} from './runtime.js';
export { validate_binding, validate_each_keys, validate_prop_bindings } from './validate.js';
export {
validate_binding,
validate_each_keys,
validate_prop_bindings,
validate_svelte_html_attribute
} from './validate.js';
export { raf } from './timing.js';
export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';

@ -9,9 +9,17 @@ import { svelte_html_duplicate_attribute } from '../../shared/warnings.js';
*/
export function svelte_html(payload, attributes) {
for (const name in attributes) {
let value = attributes[name];
if (payload.htmlAttributes.has(name)) {
svelte_html_duplicate_attribute(name);
if (name === 'class') {
// Don't bother deduplicating class names, the browser handles it just fine
value = `${payload.htmlAttributes.get(name)} ${value}`;
} else {
svelte_html_duplicate_attribute(name);
}
}
payload.htmlAttributes.set(name, escape_html(attributes[name], true));
payload.htmlAttributes.set(name, escape_html(value, true));
}
}

Loading…
Cancel
Save