All files / src/components For.ts

100% Statements 81/81
100% Branches 31/31
100% Functions 1/1
100% Lines 81/81

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 1381x 1x 1x 1x 1x                                   1x 13x 13x   13x 13x       13x   13x 26x   26x 26x     26x 26x 26x 26x     26x 124x 124x 124x   124x       95x 95x 95x       95x 95x 95x   95x 95x 95x     95x   24x 23x 24x   1x 1x 95x 95x     95x 95x 95x 95x 95x 95x 124x   29x 29x 29x 124x 124x     26x 53x 24x 24x 53x       26x 23x 23x   23x 23x   23x 23x   23x 124x 124x   95x 124x   6x 29x   23x 23x 124x 124x 23x     26x 13x   13x 13x  
import { createEffect } from '../primitives/effect';
import { createRoot, onCleanup } from '../lifecycle/lifecycle';
import { createSignal } from '../primitives/signal';
import { devWarning } from '../error';
import { longestIncreasingSubsequence } from '../internal/lis'; // Import the LIS helper
import type { Disposer, SignalGetter, SignalSetter } from '../types';
 
type ForProps<T> = {
  each: SignalGetter<T[]>;
  // The children function must return a Node
  children: (item: SignalGetter<T>, index: SignalGetter<number>) => Node;
  // UPDATE THIS LINE: Add the index parameter
  key?: (item: T, index: number) => string | number;
};
 
type MappedItem<T> = {
  node: Node;
  disposer: Disposer;
  setSignal: SignalSetter<T>;
  setIndex: SignalSetter<number>;
};
 
export function For<T>(props: ForProps<T>): Node {
  const { each, children, key } = props; // Destructure the new key prop
  const container = document.createDocumentFragment();
  // The endMarker is a stable anchor node in the DOM.
  const endMarker = document.createTextNode('');
  container.appendChild(endMarker);
 
  // The map now uses `any` for the key, as it can be the item or a string/number
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let mappedItems = new Map<any, MappedItem<T>>();
 
  createEffect(() => {
    const newItems = each();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const newMappedItems = new Map<any, MappedItem<T>>();
    const parent = endMarker.parentNode!;
 
    // Dev-mode warning if `key` is missing for primitive arrays
    devWarning(
      !!key || !newItems.some(item => typeof item !== 'object' || item === null),
      'The `For` component is being used with an array of primitives without a `key` prop. This can lead to inefficient re-rendering. Please provide a `key` function.'
    );
 
    // Pass 1: Create new items and update existing ones
    for (let i = 0; i < newItems.length; i++) {
      const itemData = newItems[i];
      const itemKey = key ? key(itemData, i) : itemData;
      let mappedItem = mappedItems.get(itemKey);
 
      if (!mappedItem) {
        // Item is new, create it within its own lifecycle root.
 
        // Item is new. Define placeholders for the values we'll create.
        let node: Node;
        let setSignal: SignalSetter<T>;
        let setIndex: SignalSetter<number>;
 
        // Create a root to manage the lifecycle of this new item.
        // It populates our placeholder variables.
        const disposer = createRoot(() => {
          const [itemSignal, setItemSignal] = createSignal(itemData);
          const [indexSignal, setIndexSignal] = createSignal(i);
 
          setSignal = setItemSignal;
          setIndex = setIndexSignal;
          node = children(itemSignal, indexSignal);
 
          // When this item's root is disposed, remove its node from the DOM.
          onCleanup(() => {
            // Check if the node is an Element before calling remove()
            if (node instanceof Element) {
              node.remove();
            } else if (node.parentNode) {
              // Fallback for Text nodes or other node types
              node.parentNode.removeChild(node);
            }
          });
        });
 
        // Now, construct the MappedItem object with the populated values.
        mappedItem = {
          node: node!, // Add '!' to assert that `node` is not undefined
          disposer: disposer,
          setSignal: setSignal!, // Add '!' to assert that `setSignal` is not undefined
          setIndex: setIndex!, // Add '!' to assert that `setIndex` is not undefined
        };
      } else {
        // Item already exists, update its data and index signals.
        mappedItem.setSignal(itemData);
        mappedItem.setIndex(i);
      }
      newMappedItems.set(itemKey, mappedItem);
    }
 
    // Pass 2: Remove old items
    for (const [itemKey, item] of mappedItems.entries()) {
      if (!newMappedItems.has(itemKey)) {
        item.disposer(); // This triggers onCleanup which removes the node
      }
    }
 
    // --- THE UPGRADE: LIS-based Reconciliation ---
    // Pass 3: Perform efficient DOM moves
    if (newItems.length > 0) {
      const oldMap = new Map(Array.from(mappedItems.values()).map((item, i) => [item.node, i]));
      const newNodes = newItems.map((item, i) => newMappedItems.get(key ? key(item, i) : item)!.node);
      
      const seq = newNodes.map(node => oldMap.get(node));
      const lis = longestIncreasingSubsequence(seq.map(i => i === undefined ? -1 : i));
 
      let cur = lis.length - 1;
      let next: Node | null = endMarker;
 
      for (let i = newNodes.length - 1; i >= 0; i--) {
        const node = newNodes[i];
        if (seq[i] === undefined) {
          // It's a new node, insert it.
          parent.insertBefore(node, next);
        } else if (cur < 0 || i !== lis[cur]) {
          // It's a moved node.
          parent.insertBefore(node, next);
        } else {
          // It's a stable node.
          cur--;
        }
        next = node;
      }
    }
 
    // Update the master map for the next run.
    mappedItems = newMappedItems;
  });
 
  return container;
}