From 028dba829fabda81e841d884b7b31f4353a70c90 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 08:55:59 -0500 Subject: [PATCH] each blocks work! --- .../src/internal/client/dom/blocks/each.js | 118 +++++++++++++++--- .../samples/async-each-await-item/_config.js | 1 + 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 3c600a06f8..4414948df5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -20,7 +20,8 @@ import { clear_text_content, create_text, get_first_child, - get_next_sibling + get_next_sibling, + should_defer_append } from '../operations.js'; import { block, @@ -35,10 +36,10 @@ import { source, mutable_source, internal_set } from '../../reactivity/sources.j import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; import { queue_micro_task } from '../task.js'; -import { active_effect, active_reaction, get } from '../../runtime.js'; +import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { find_boundary } from './boundary.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -64,17 +65,18 @@ export function index(_, i) { * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks * @param {EachState} state - * @param {EachItem[]} items + * @param {EachItem[]} to_destroy * @param {null | Node} controlled_anchor - * @param {Map} items_map */ -function pause_effects(state, items, controlled_anchor, items_map) { +function pause_effects(state, to_destroy, controlled_anchor) { + var items_map = state.items; + /** @type {TransitionManager[]} */ var transitions = []; - var length = items.length; + var length = to_destroy.length; for (var i = 0; i < length; i++) { - pause_children(items[i].e, transitions, true); + pause_children(to_destroy[i].e, transitions, true); } var is_controlled = length > 0 && transitions.length === 0 && controlled_anchor !== null; @@ -87,12 +89,12 @@ function pause_effects(state, items, controlled_anchor, items_map) { clear_text_content(parent_node); parent_node.append(/** @type {Element} */ (controlled_anchor)); items_map.clear(); - link(state, items[0].prev, items[length - 1].next); + link(state, to_destroy[0].prev, to_destroy[length - 1].next); } run_out_transitions(transitions, () => { for (var i = 0; i < length; i++) { - var item = items[i]; + var item = to_destroy[i]; if (!is_controlled) { items_map.delete(item.k); link(state, item.prev, item.next); @@ -139,6 +141,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var boundary = find_boundary(active_effect); + /** @type {Map} */ + var pending_items = new Map(); + // TODO: ideally we could use derived for runes mode but because of the ability // to use a store which can be mutated, we can't do that here as mutating a store // will still result in the collection array being the same from the store @@ -151,8 +156,21 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {V[]} */ var array; + /** @type {Effect} */ + var each_effect; + function commit() { - reconcile(array, state, anchor, render_fn, flags, get_key, get_collection); + reconcile( + each_effect, + array, + state, + pending_items, + anchor, + render_fn, + flags, + get_key, + get_collection + ); if (fallback_fn !== null) { if (array.length === 0) { @@ -170,6 +188,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } block(() => { + // store a reference to the effect so that we can update the start/end nodes in reconciliation + each_effect ??= /** @type {Effect} */ (active_effect); + array = get(each_array); var length = array.length; @@ -247,7 +268,42 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - commit(); + var defer = boundary !== null && should_defer_append(); + + if (defer) { + for (i = 0; i < length; i += 1) { + value = array[i]; + key = get_key(value, i); + + var existing = state.items.get(key) ?? pending_items.get(key); + + if (existing) { + // update before reconciliation, to trigger any async updates + if ((flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0) { + update_item(existing, value, i, flags); + } + } else { + var item = create_item( + null, + state, + null, + null, + value, + key, + i, + render_fn, + flags, + get_collection + ); + + pending_items.set(key, item); + } + } + + add_boundary_callback(boundary, commit); + } else { + commit(); + } } if (mismatch) { @@ -272,8 +328,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** * Add, remove, or reorder items output by an each block as its input changes * @template V + * @param {Effect} each_effect * @param {Array} array * @param {EachState} state + * @param {Map} pending_items * @param {Element | Comment | Text} anchor * @param {(anchor: Node, item: MaybeSource, index: number | Source, collection: () => V[]) => void} render_fn * @param {number} flags @@ -281,7 +339,17 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @param {() => V[]} get_collection * @returns {void} */ -function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) { +function reconcile( + each_effect, + array, + state, + pending_items, + anchor, + render_fn, + flags, + get_key, + get_collection +) { var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; @@ -333,7 +401,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); - item = items.get(key); + item = items.get(key) ?? pending_items.get(key); if (item === undefined) { var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; @@ -468,7 +536,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti } } - pause_effects(state, to_destroy, controlled_anchor, items); + pause_effects(state, to_destroy, controlled_anchor); } } @@ -481,8 +549,13 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti }); } - /** @type {Effect} */ (active_effect).first = state.first && state.first.e; - /** @type {Effect} */ (active_effect).last = prev && prev.e; + // TODO this seems super weird... should be `each_effect`, but that doesn't seem to work? + if (active_effect !== null) { + active_effect.first = state.first && state.first.e; + active_effect.last = prev && prev.e; + } + + pending_items.clear(); } /** @@ -506,7 +579,7 @@ function update_item(item, value, index, type) { /** * @template V - * @param {Node} anchor + * @param {Node | null} anchor * @param {EachState} state * @param {EachItem | null} prev * @param {EachItem | null} next @@ -562,7 +635,12 @@ function create_item( current_each_item = item; try { - item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating); + if (anchor === null) { + var fragment = document.createDocumentFragment(); + fragment.append((anchor = document.createComment(''))); + } + + item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating); item.e.prev = prev && prev.e; item.e.next = next && next.e; @@ -596,7 +674,7 @@ function move(item, next, anchor) { var dest = next ? /** @type {TemplateNode} */ (next.e.nodes_start) : anchor; var node = /** @type {TemplateNode} */ (item.e.nodes_start); - while (node !== end) { + while (node !== null && node !== end) { var next_node = /** @type {TemplateNode} */ (get_next_sibling(node)); dest.before(node); node = next_node; diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js index bba0c77386..dd6f228deb 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -35,6 +35,7 @@ export default test({ items[1].resolve('c'); items[2].resolve('d'); items[3].resolve('e'); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

b

c

d

e

'); }