Skip to content

Commit 7032837

Browse files
authored
fix: better handling of empty text node hydration (#10545)
* fix: better handling of empty text node hydration
1 parent 41e7dab commit 7032837

File tree

7 files changed

+55
-8
lines changed

7 files changed

+55
-8
lines changed

.changeset/heavy-comics-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: better handling of empty text node hydration

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,15 +1127,15 @@ function create_block(parent, name, nodes, context) {
11271127
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
11281128

11291129
if (use_space_template) {
1130-
// special case — we can use `$.space` instead of creating a unique template
1130+
// special case — we can use `$.space_frag` instead of creating a unique template
11311131
const id = b.id(context.state.scope.generate('text'));
11321132

11331133
process_children(trimmed, () => id, false, {
11341134
...context,
11351135
state
11361136
});
11371137

1138-
body.push(b.var(id, b.call('$.space', b.id('$$anchor'))), ...state.init);
1138+
body.push(b.var(id, b.call('$.space_frag', b.id('$$anchor'))), ...state.init);
11391139
close = b.stmt(b.call('$.close', b.id('$$anchor'), id));
11401140
} else {
11411141
/** @type {(is_text: boolean) => import('estree').Expression} */
@@ -1495,7 +1495,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
14951495

14961496
state.template.push(' ');
14971497

1498-
const text_id = get_node_id(expression(true), state, 'text');
1498+
const text_id = get_node_id(b.call('$.space', expression(true)), state, 'text');
14991499

15001500
const singular = b.stmt(
15011501
b.call(

packages/svelte/src/internal/client/render.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const comment_template = template('<!>', true);
206206
* @param {Text | Comment | Element | null} anchor
207207
*/
208208
/*#__NO_SIDE_EFFECTS__*/
209-
export function space(anchor) {
209+
export function space_frag(anchor) {
210210
/** @type {Node | null} */
211211
var node = /** @type {any} */ (open(anchor, true, space_template));
212212
// if an {expression} is empty during SSR, there might be no
@@ -216,11 +216,27 @@ export function space(anchor) {
216216
node = empty();
217217
// @ts-ignore in this case the anchor should always be a comment,
218218
// if not something more fundamental is wrong and throwing here is better to bail out early
219-
anchor.parentElement.insertBefore(node, anchor);
219+
anchor.before(node);
220220
}
221221
return node;
222222
}
223223

224+
/**
225+
* @param {Text | Comment | Element} anchor
226+
*/
227+
/*#__NO_SIDE_EFFECTS__*/
228+
export function space(anchor) {
229+
// if an {expression} is empty during SSR, there might be no
230+
// text node to hydrate (or an anchor comment is falsely detected instead)
231+
// — we must therefore create one
232+
if (hydrating && anchor.nodeType !== 3) {
233+
const node = empty();
234+
anchor.before(node);
235+
return node;
236+
}
237+
return anchor;
238+
}
239+
224240
/**
225241
* @param {null | Text | Comment | Element} anchor
226242
*/
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button type="button">Update Text</button><div></div>`,
5+
6+
async test({ assert, target }) {
7+
const btn = target.querySelector('button');
8+
9+
await btn?.click();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`<button type="button">Update Text</button><div>updated</div>`
13+
);
14+
}
15+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let text = $state('');
3+
4+
function update_text() {
5+
text = 'updated';
6+
}
7+
</script>
8+
9+
<button type="button" on:click={update_text}>Update Text</button>
10+
11+
<div>{text}</div>

packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function Each_string_template($$anchor, $$props) {
1717
1,
1818
($$anchor, thing, $$index) => {
1919
/* Init */
20-
var text = $.space($$anchor);
20+
var text = $.space_frag($$anchor);
2121

2222
/* Update */
2323
$.text_effect(text, () => `${$.stringify($.unwrap(thing))}, `);

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function Function_prop_no_getter($$anchor, $$props) {
2323
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
2424
children: ($$anchor, $$slotProps) => {
2525
/* Init */
26-
var text = $.space($$anchor);
26+
var text = $.space_frag($$anchor);
2727

2828
/* Update */
2929
$.text_effect(text, () => `clicks: ${$.stringify($.get(count))}`);
@@ -33,4 +33,4 @@ export default function Function_prop_no_getter($$anchor, $$props) {
3333

3434
$.close_frag($$anchor, fragment);
3535
$.pop();
36-
}
36+
}

0 commit comments

Comments
 (0)