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
Show file tree
Hide file tree
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
Expand Up @@ -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(
Expand All @@ -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];
}
Expand Down Expand Up @@ -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
);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>} */
Expand Down Expand Up @@ -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(
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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') {
Expand Down
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();
};
});
Expand Down
8 changes: 4 additions & 4 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>`;
}
}

Expand Down
Loading