diff --git a/.changeset/funny-dragons-double.md b/.changeset/funny-dragons-double.md new file mode 100644 index 0000000000..6a539769de --- /dev/null +++ b/.changeset/funny-dragons-double.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: simpler hydration of CSS custom property wrappers diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 519423a5ce..fa96cfe296 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -896,30 +896,35 @@ function serialize_inline_component(node, component_name, context) { '$.spread_props', ...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)) ); - /** @param {import('estree').Identifier} node_id */ - let fn = (node_id) => - b.call( + + /** @param {import('estree').Expression} node_id */ + let fn = (node_id) => { + return b.call( context.state.options.dev ? b.call('$.validate_component', b.id(component_name)) : component_name, node_id, props_expression ); + }; if (bind_this !== null) { const prev = fn; - fn = (node_id) => - serialize_bind_this( + + fn = (node_id) => { + return serialize_bind_this( /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (bind_this), context, prev(node_id) ); + }; } if (node.type === 'SvelteComponent') { const prev = fn; + fn = (node_id) => { - let component = b.call( + return b.call( '$.component', b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), b.arrow( @@ -933,31 +938,26 @@ function serialize_inline_component(node, component_name, context) { ]) ) ); - return component; }; } + const statements = [...snippet_declarations, ...binding_initializers]; + if (Object.keys(custom_css_props).length > 0) { - const prev = fn; - fn = (node_id) => - b.call( - '$.css_props', - node_id, - // TODO would be great to do this at runtime instead. Svelte 4 also can't handle cases today - // where it's not statically determinable whether the component is used in a svg or html context - context.state.metadata.namespace === 'svg' || context.state.metadata.namespace === 'mathml' - ? b.false - : b.true, - b.thunk(b.object(custom_css_props)), - b.arrow([b.id('$$node')], prev(b.id('$$node'))) - ); - } + context.state.template.push( + context.state.metadata.namespace === 'svg' + ? '' + : '
' + ); - const statements = [ - ...snippet_declarations, - ...binding_initializers, - b.stmt(fn(context.state.node)) - ]; + statements.push( + b.stmt(b.call('$.css_props', context.state.node, b.thunk(b.object(custom_css_props)))), + b.stmt(fn(b.member(context.state.node, b.id('lastChild')))) + ); + } else { + context.state.template.push(''); + statements.push(b.stmt(fn(context.state.node))); + } return statements.length > 1 ? b.block(statements) : statements[0]; } @@ -2947,8 +2947,6 @@ export const template_visitors = { } }, Component(node, context) { - context.state.template.push(''); - const binding = context.state.scope.get( node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name ); @@ -2974,13 +2972,10 @@ export const template_visitors = { context.state.init.push(component); }, SvelteSelf(node, context) { - context.state.template.push(''); const component = serialize_inline_component(node, context.state.analysis.name, context); context.state.init.push(component); }, SvelteComponent(node, context) { - context.state.template.push(''); - let component = serialize_inline_component(node, '$$component', context); context.state.init.push(component); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 6deb02385f..eb04cd9397 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -907,7 +907,6 @@ function serialize_element_spread_attributes( * @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node * @param {string | import('estree').Expression} component_name * @param {import('./types').ComponentContext} context - * @returns {import('estree').Statement} */ function serialize_inline_component(node, component_name, context) { /** @type {Array} */ @@ -1103,6 +1102,10 @@ function serialize_inline_component(node, component_name, context) { ) ); + if (snippet_declarations.length > 0) { + statement = b.block([...snippet_declarations, statement]); + } + if (custom_css_props.length > 0) { statement = b.stmt( b.call( @@ -1113,13 +1116,13 @@ function serialize_inline_component(node, component_name, context) { b.thunk(b.block([statement])) ) ); - } - if (snippet_declarations.length > 0) { - statement = b.block([...snippet_declarations, statement]); + context.state.template.push(t_statement(statement)); + } else { + context.state.template.push(block_open); + context.state.template.push(t_statement(statement)); + context.state.template.push(block_close); } - - return statement; } /** @@ -1666,29 +1669,17 @@ const template_visitors = { } }, Component(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component(node, node.name, context); - state.template.push(t_statement(call)); - state.template.push(block_close); + serialize_inline_component(node, node.name, context); }, SvelteSelf(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component(node, context.state.analysis.name, context); - state.template.push(t_statement(call)); - state.template.push(block_close); + serialize_inline_component(node, context.state.analysis.name, context); }, SvelteComponent(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component( + serialize_inline_component( node, /** @type {import('estree').Expression} */ (context.visit(node.expression)), context ); - state.template.push(t_statement(call)); - state.template.push(block_close); }, LetDirective(node, { state }) { if (node.expression && node.expression.type !== 'Identifier') { diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index 042b6aa9e5..b2534a4652 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -1,64 +1,35 @@ -import { namespace_svg } from '../../../../constants.js'; -import { hydrate_anchor, hydrate_start, hydrating } from '../hydration.js'; -import { empty } from '../operations.js'; +import { hydrating, set_hydrate_nodes } from '../hydration.js'; import { render_effect } from '../../reactivity/effects.js'; /** - * @param {Element | Text | Comment} anchor - * @param {boolean} is_html - * @param {() => Record} props - * @param {(anchor: Element | Text | Comment) => any} component + * @param {HTMLDivElement | SVGGElement} element + * @param {() => Record} get_styles * @returns {void} */ -export function css_props(anchor, is_html, props, component) { - /** @type {HTMLElement | SVGElement} */ - let element; - - /** @type {Text | Comment} */ - let component_anchor; - +export function css_props(element, get_styles) { if (hydrating) { - // Hydration: css props element is surrounded by a ssr comment ... - element = /** @type {HTMLElement | SVGElement} */ (hydrate_start); - // ... and the child(ren) of the css props element is also surround by a ssr comment - component_anchor = /** @type {Comment} */ ( - hydrate_anchor(/** @type {Comment} */ (element.firstChild)) + set_hydrate_nodes( + /** @type {import('#client').TemplateNode[]} */ ([...element.childNodes]).slice(0, -1) ); - } else { - if (is_html) { - element = document.createElement('div'); - element.style.display = 'contents'; - } else { - element = document.createElementNS(namespace_svg, 'g'); - } - - anchor.before(element); - component_anchor = element.appendChild(empty()); } - component(component_anchor); - render_effect(() => { - /** @type {Record} */ - let current_props = {}; - render_effect(() => { - const next_props = props(); + var styles = get_styles(); - for (const key in current_props) { - if (!(key in next_props)) { + for (var key in styles) { + var value = styles[key]; + + if (value) { + element.style.setProperty(key, value); + } else { element.style.removeProperty(key); } } - - for (const key in next_props) { - element.style.setProperty(key, next_props[key]); - } - - current_props = next_props; }); return () => { + // TODO use `teardown` instead of creating a nested effect, post-https://github.com/sveltejs/svelte/pull/11936 element.remove(); }; }); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3b6a0158b3..7f7b0ebe98 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -162,15 +162,15 @@ export function attr(name, value, boolean) { export function css_props(payload, is_html, props, component) { const styles = style_object_to_string(props); if (is_html) { - payload.out += `
`; + payload.out += `
`; } else { - payload.out += ``; + payload.out += ``; } component(); if (is_html) { - payload.out += `
`; + payload.out += `
`; } else { - payload.out += ``; + payload.out += ``; } }