fix: link offscreen items and last effect in each block correctly (#17240)

* fix: link offscreen items and last effect in each block correctly

It's possible that due to how new elements are inserted into the array that `effect.last` is wrong. We need to ensure it is really the last item to keep items properly connected to the graph. In addition we link offscreen items after all onscreen items, to ensure they don't have wrong pointers.

Fixes #17201

* revert #17244

* add test
pull/17253/head
Simon H 2 months ago committed by GitHub
parent 1bc3ae7cbf
commit da4cd0e359
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: link offscreen items and last effect in each block correctly

@ -507,6 +507,8 @@ function reconcile(state, array, anchor, flags, get_key) {
current = item.next;
}
let has_offscreen_items = items.size > length;
if (current !== null || seen !== undefined) {
var to_destroy = seen === undefined ? [] : array_from(seen);
@ -520,6 +522,8 @@ function reconcile(state, array, anchor, flags, get_key) {
var destroy_length = to_destroy.length;
has_offscreen_items = items.size - destroy_length > length;
if (destroy_length > 0) {
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
@ -537,6 +541,18 @@ function reconcile(state, array, anchor, flags, get_key) {
}
}
// Append offscreen items at the end
if (has_offscreen_items) {
for (const item of items.values()) {
if (!item.o) {
link(state, prev, item);
prev = item;
}
}
}
state.effect.last = prev && prev.e;
if (is_animated) {
queue_micro_task(() => {
if (to_animate === undefined) return;
@ -641,10 +657,6 @@ function link(state, prev, next) {
state.first = next;
state.effect.first = next && next.e;
} else {
if (prev.e === state.effect.last && next !== null) {
state.effect.last = next.e;
}
if (prev.e.next) {
prev.e.next.prev = null;
}
@ -653,13 +665,7 @@ function link(state, prev, next) {
prev.e.next = next && next.e;
}
if (next === null) {
state.effect.last = prev && prev.e;
} else {
if (next.e === state.effect.last && prev === null) {
state.effect.last = next.e.prev;
}
if (next !== null) {
if (next.e.prev) {
next.e.prev.next = null;
}

@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [add4, add5, modify3] = target.querySelectorAll('button');
add4.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
1423`
);
add5.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
14523`
);
modify3.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
1452updated`
);
}
});

@ -0,0 +1,11 @@
<script>
let list = $state([{ id: 1, text: '1' }, { id: 2, text: '2' }, { id: 3, text: '3' }]);
</script>
<button onclick={() => list = [list[0], { id: 4, text: '4' }, ...list.slice(1)]}>add 4</button>
<button onclick={() => list = [list[0], list[1], { id: 5, text: '5' }, ...list.slice(2)]}>add 5</button>
<button onclick={() => list.at(-1).text = 'updated'}>modify 3</button>
{#each list as item (item.id)}
{item.text}
{/each}
Loading…
Cancel
Save