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 += ``;
}
}