Skip to content

feat: simpler hydration of CSS custom property wrappers #11948

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-dragons-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: simpler hydration of CSS custom property wrappers
Original file line number Diff line number Diff line change
@@ -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'
? '<g><!></g>'
: '<div style="display: contents"><!></div>'
);

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);
Original file line number Diff line number Diff line change
@@ -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<import('estree').Property[] | import('estree').Expression>} */
@@ -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') {
57 changes: 14 additions & 43 deletions packages/svelte/src/internal/client/dom/blocks/css-props.js
Original file line number Diff line number Diff line change
@@ -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<string, string>} props
* @param {(anchor: Element | Text | Comment) => any} component
* @param {HTMLDivElement | SVGGElement} element
* @param {() => Record<string, string>} 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<string, string>} */
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();
};
});
8 changes: 4 additions & 4 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
@@ -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 += `<div style="display: contents; ${styles}"><!--[-->`;
payload.out += `<div style="display: contents; ${styles}">`;
} else {
payload.out += `<g style="${styles}"><!--[-->`;
payload.out += `<g style="${styles}">`;
}
component();
if (is_html) {
payload.out += `<!--]--></div>`;
payload.out += `<!----></div>`;
} else {
payload.out += `<!--]--></g>`;
payload.out += `<!----></g>`;
}
}