fix: better handling of empty text node hydration (#10545)

* fix: better handling of empty text node hydration
pull/10553/head
Dominic Gannaway 10 months ago committed by GitHub
parent 41e7dab755
commit 7032837ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: better handling of empty text node hydration

@ -1127,7 +1127,7 @@ function create_block(parent, name, nodes, context) {
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag'); trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
if (use_space_template) { if (use_space_template) {
// special case — we can use `$.space` instead of creating a unique template // special case — we can use `$.space_frag` instead of creating a unique template
const id = b.id(context.state.scope.generate('text')); const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, { process_children(trimmed, () => id, false, {
@ -1135,7 +1135,7 @@ function create_block(parent, name, nodes, context) {
state state
}); });
body.push(b.var(id, b.call('$.space', b.id('$$anchor'))), ...state.init); body.push(b.var(id, b.call('$.space_frag', b.id('$$anchor'))), ...state.init);
close = b.stmt(b.call('$.close', b.id('$$anchor'), id)); close = b.stmt(b.call('$.close', b.id('$$anchor'), id));
} else { } else {
/** @type {(is_text: boolean) => import('estree').Expression} */ /** @type {(is_text: boolean) => import('estree').Expression} */
@ -1495,7 +1495,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
state.template.push(' '); state.template.push(' ');
const text_id = get_node_id(expression(true), state, 'text'); const text_id = get_node_id(b.call('$.space', expression(true)), state, 'text');
const singular = b.stmt( const singular = b.stmt(
b.call( b.call(

@ -206,7 +206,7 @@ const comment_template = template('<!>', true);
* @param {Text | Comment | Element | null} anchor * @param {Text | Comment | Element | null} anchor
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function space(anchor) { export function space_frag(anchor) {
/** @type {Node | null} */ /** @type {Node | null} */
var node = /** @type {any} */ (open(anchor, true, space_template)); var node = /** @type {any} */ (open(anchor, true, space_template));
// if an {expression} is empty during SSR, there might be no // if an {expression} is empty during SSR, there might be no
@ -216,11 +216,27 @@ export function space(anchor) {
node = empty(); node = empty();
// @ts-ignore in this case the anchor should always be a comment, // @ts-ignore in this case the anchor should always be a comment,
// if not something more fundamental is wrong and throwing here is better to bail out early // if not something more fundamental is wrong and throwing here is better to bail out early
anchor.parentElement.insertBefore(node, anchor); anchor.before(node);
} }
return node; return node;
} }
/**
* @param {Text | Comment | Element} anchor
*/
/*#__NO_SIDE_EFFECTS__*/
export function space(anchor) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate (or an anchor comment is falsely detected instead)
// — we must therefore create one
if (hydrating && anchor.nodeType !== 3) {
const node = empty();
anchor.before(node);
return node;
}
return anchor;
}
/** /**
* @param {null | Text | Comment | Element} anchor * @param {null | Text | Comment | Element} anchor
*/ */

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button type="button">Update Text</button><div></div>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(
target.innerHTML,
`<button type="button">Update Text</button><div>updated</div>`
);
}
});

@ -0,0 +1,11 @@
<script>
let text = $state('');
function update_text() {
text = 'updated';
}
</script>
<button type="button" on:click={update_text}>Update Text</button>
<div>{text}</div>

@ -17,7 +17,7 @@ export default function Each_string_template($$anchor, $$props) {
1, 1,
($$anchor, thing, $$index) => { ($$anchor, thing, $$index) => {
/* Init */ /* Init */
var text = $.space($$anchor); var text = $.space_frag($$anchor);
/* Update */ /* Update */
$.text_effect(text, () => `${$.stringify($.unwrap(thing))}, `); $.text_effect(text, () => `${$.stringify($.unwrap(thing))}, `);

@ -23,7 +23,7 @@ export default function Function_prop_no_getter($$anchor, $$props) {
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
children: ($$anchor, $$slotProps) => { children: ($$anchor, $$slotProps) => {
/* Init */ /* Init */
var text = $.space($$anchor); var text = $.space_frag($$anchor);
/* Update */ /* Update */
$.text_effect(text, () => `clicks: ${$.stringify($.get(count))}`); $.text_effect(text, () => `clicks: ${$.stringify($.get(count))}`);

Loading…
Cancel
Save