diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHTML.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHTML.js index 082c82aa81..f6e4f50c1a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHTML.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHTML.js @@ -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))))); + } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-html.js b/packages/svelte/src/internal/client/dom/blocks/svelte-html.js new file mode 100644 index 0000000000..b69b6c7623 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-html.js @@ -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} get_attributes + * @returns {void} + */ +export function svelte_html(get_attributes) { + const node = document.documentElement; + const own = {}; + + /** @type {Record>} to check who set the last value of each attribute */ + // @ts-expect-error + const current_setters = (node.__attributes_setters ??= {}); + + /** @type {Record} */ + 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]); + } + } + }); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index b706e52a53..f38deeddb8 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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'; diff --git a/packages/svelte/src/internal/server/blocks/svelte-html.js b/packages/svelte/src/internal/server/blocks/svelte-html.js index 31ac205c85..adda28321f 100644 --- a/packages/svelte/src/internal/server/blocks/svelte-html.js +++ b/packages/svelte/src/internal/server/blocks/svelte-html.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)); } }