diff --git a/.changeset/smooth-cameras-appear.md b/.changeset/smooth-cameras-appear.md
new file mode 100644
index 000000000000..77dfb049f2cc
--- /dev/null
+++ b/.changeset/smooth-cameras-appear.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: ensure correct each block element is moved during reconcilation
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 6a426db0d419..6892eb3b3c5c 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
@@ -1646,7 +1646,11 @@ export const template_visitors = {
add_template(template_name, args);
- body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
+ body.push(
+ b.var(id, b.call(template_name, b.id('$$anchor'))),
+ ...state.before_init,
+ ...state.init
+ );
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
@@ -1684,7 +1688,7 @@ export const template_visitors = {
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
- body.push(b.var(id, b.call('$.comment')));
+ body.push(b.var(id, b.call('$.comment', b.id('$$anchor'))));
} else {
let flags = TEMPLATE_FRAGMENT;
@@ -1697,7 +1701,7 @@ export const template_visitors = {
b.literal(flags)
]);
- body.push(b.var(id, b.call(template_name)));
+ body.push(b.var(id, b.call(template_name, b.id('$$anchor'))));
}
body.push(...state.before_init, ...state.init);
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index e20d4ec41fa9..45b0b0dd6459 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -291,7 +291,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
item = items.get(key);
if (item === undefined) {
- var child_anchor = current ? get_first_node(current.e) : anchor;
+ var effect_dom = current?.e.dom;
+ var child_anchor = /** @type {Node} */ (
+ effect_dom ? (is_array(effect_dom) ? effect_dom[0] : effect_dom) : anchor
+ );
prev = create_item(child_anchor, prev, prev.next, value, key, i, render_fn, flags);
@@ -486,7 +489,10 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
* @returns {import('#client').TemplateNode}
*/
function get_adjusted_first_node(dom, effect) {
- if ((dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') || dom.nodeType === 8) {
+ if (
+ (dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') ||
+ (dom.nodeType === 8 && /** @type {Comment} */ (dom).data !== '[')
+ ) {
var adjusted = effect.first;
var next;
while (adjusted !== null) {
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index 6873adf70ce2..8ab6d58d6b7e 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -82,7 +82,7 @@ function html_to_dom(target, effect, value, svg, mathml) {
var child = /** @type {Text | Element | Comment} */ (node.firstChild);
target.before(child);
if (effect !== null) {
- push_template_node(child, effect);
+ push_template_node(child, null, effect);
}
return child;
}
@@ -98,7 +98,7 @@ function html_to_dom(target, effect, value, svg, mathml) {
}
if (effect !== null) {
- push_template_node(nodes, effect);
+ push_template_node(nodes, null, effect);
}
return nodes;
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 6e0740b937b3..611eb6395fa1 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -140,7 +140,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
swap_block_dom(element_effect, prev_element, element);
prev_element.remove();
} else {
- push_template_node(element, element_effect);
+ push_template_node(element, null, element_effect);
}
if (render_fn) {
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index b00a3a242b1e..c5fb0f277115 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -1,7 +1,7 @@
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
+import { HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index 7342b71edadd..5ee3611b5e3d 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -9,10 +9,12 @@ import { queue_micro_task } from './task.js';
/**
* @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T
* @param {T} dom
+ * @param {import("#client").TemplateNode | null} anchor
* @param {import("#client").Effect} effect
*/
export function push_template_node(
dom,
+ anchor,
effect = /** @type {import('#client').Effect} */ (current_effect)
) {
var current_dom = effect.dom;
@@ -22,11 +24,23 @@ export function push_template_node(
if (!is_array(current_dom)) {
current_dom = effect.dom = [current_dom];
}
+ // If we have an existing anchor, then we should ensure that we insert the DOM contents
+ // before that anchor position. This ensures we match what is reflected on the document to
+ // as what is reflected in the effect.dom (we always insert before the anchor).
+ const anchor_index = anchor !== null ? current_dom.indexOf(anchor) : null;
if (is_array(dom)) {
- current_dom.push(...dom);
+ if (anchor_index !== null) {
+ current_dom.splice(anchor_index, 0, ...dom);
+ } else {
+ current_dom.push(...dom);
+ }
} else {
- current_dom.push(dom);
+ if (anchor_index !== null) {
+ current_dom.splice(anchor_index, 0, dom);
+ } else {
+ current_dom.push(dom);
+ }
}
}
return dom;
@@ -45,9 +59,9 @@ export function template(content, flags) {
/** @type {Node} */
var node;
- return () => {
+ return (/** @type {Element | Comment | null} */ prev_anchor) => {
if (hydrating) {
- push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
+ push_template_node(is_fragment ? hydrate_nodes : hydrate_start, prev_anchor);
return hydrate_start;
}
@@ -61,7 +75,8 @@ export function template(content, flags) {
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
- : /** @type {import('#client').TemplateNode} */ (clone)
+ : /** @type {import('#client').TemplateNode} */ (clone),
+ prev_anchor
);
return clone;
@@ -106,9 +121,9 @@ export function ns_template(content, flags, ns = 'svg') {
/** @type {Element | DocumentFragment} */
var node;
- return () => {
+ return (/** @type {Element | Comment | null} */ prev_anchor) => {
if (hydrating) {
- push_template_node(is_fragment ? hydrate_nodes : hydrate_start);
+ push_template_node(is_fragment ? hydrate_nodes : hydrate_start, prev_anchor);
return hydrate_start;
}
@@ -130,7 +145,8 @@ export function ns_template(content, flags, ns = 'svg') {
push_template_node(
is_fragment
? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes])
- : /** @type {import('#client').TemplateNode} */ (clone)
+ : /** @type {import('#client').TemplateNode} */ (clone),
+ prev_anchor
);
return clone;
@@ -208,7 +224,7 @@ function run_scripts(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function text(anchor) {
- if (!hydrating) return push_template_node(empty());
+ if (!hydrating) return push_template_node(empty(), null);
var node = hydrate_start;
@@ -218,21 +234,24 @@ export function text(anchor) {
anchor.before((node = empty()));
}
- push_template_node(node);
+ push_template_node(node, null);
return node;
}
-export function comment() {
+/**
+ * @param {Element | Comment | null} prev_anchor
+ */
+export function comment(prev_anchor) {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
- push_template_node(hydrate_nodes);
+ push_template_node(hydrate_nodes, prev_anchor);
return hydrate_start;
}
var frag = document.createDocumentFragment();
var anchor = empty();
frag.append(anchor);
- push_template_node([anchor]);
+ push_template_node([anchor], prev_anchor);
return frag;
}
diff --git a/packages/svelte/tests/migrate/samples/svelte-element/output.svelte b/packages/svelte/tests/migrate/samples/svelte-element/output.svelte
index 09a9960f56ad..4dc05f351f81 100644
--- a/packages/svelte/tests/migrate/samples/svelte-element/output.svelte
+++ b/packages/svelte/tests/migrate/samples/svelte-element/output.svelte
@@ -3,4 +3,4 @@
first
message 1
`, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `first
message 1
message 2
` + ); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `first
message 1
message 2
` + ); + + flushSync(() => { + btn1.click(); + }); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `first
message 1
message 2
message 3
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte new file mode 100644 index 000000000000..869ccdc8dd78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte @@ -0,0 +1,23 @@ + + + + +{#each messages as msg, i (`${msg.id}_${msg.tmpId ?? ""}`)} + {#if i === 0} +first
+ {/if} +{msg.content}
+{/each} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 0e193af12d5f..5493a1318eaf 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -7,14 +7,14 @@ var root = $.template(` `, 1); export default function Bind_component_snippet($$anchor) { var snippet = ($$anchor) => { - var fragment = root_1(); + var fragment = root_1($$anchor); $.append($$anchor, fragment); }; let value = $.source(''); const _snippet = snippet; - var fragment_1 = root(); + var fragment_1 = root($$anchor); var node = $.first_child(fragment_1); TextInput(node, { diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index c766ee0a79b4..8ed7cb6ad9fd 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Bind_this($$anchor) { - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index aa4d22d99a6d..b7324aabb099 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -7,7 +7,7 @@ export default function Main($$anchor) { // needs to be a snapshot test because jsdom does auto-correct the attribute casing let x = 'test'; let y = () => 'test'; - var fragment = root(); + var fragment = root($$anchor); var div = $.first_child(fragment); var svg = $.sibling($.sibling(div, true)); var custom_element = $.sibling($.sibling(svg, true)); diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 5d5e47faf2d6..120ea6f4e0d2 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Each_string_template($$anchor) { - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); $.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => { diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 2d25ba9fd25c..f20857e33b21 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Function_prop_no_getter($$anchor) { } const plusOne = (num) => num + 1; - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); Button(node, { diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index 92354d8f1483..ef0633e0d62f 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -4,7 +4,7 @@ import * as $ from "svelte/internal/client"; var root = $.template(`