Add keyed list reconciliation to reactive-list
Items with :key attributes are matched by key across renders — existing DOM nodes are reused, stale nodes removed, new nodes inserted in order. Falls back to clear-and-rerender without keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-08T16:03:46Z";
|
||||
var SX_VERSION = "2026-03-08T16:12:12Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1686,22 +1686,59 @@ return (isSxTruthy(testFn()) ? (function() {
|
||||
return marker;
|
||||
})(); };
|
||||
|
||||
// render-list-item
|
||||
var renderListItem = function(mapFn, item, env, ns) { return (isSxTruthy(isLambda(mapFn)) ? renderLambdaDom(mapFn, [item], env, ns) : renderToDom(apply(mapFn, [item]), env, ns)); };
|
||||
|
||||
// extract-key
|
||||
var extractKey = function(node, index) { return (function() {
|
||||
var k = domGetAttr(node, "key");
|
||||
return (isSxTruthy(k) ? (domRemoveAttr(node, "key"), k) : (function() {
|
||||
var dk = domGetData(node, "key");
|
||||
return (isSxTruthy(dk) ? (String(dk)) : (String("__idx_") + String(index)));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// reactive-list
|
||||
var reactiveList = function(mapFn, itemsSig, env, ns) { return (function() {
|
||||
var container = createFragment();
|
||||
var marker = createComment("island-list");
|
||||
var keyMap = {};
|
||||
var keyOrder = [];
|
||||
domAppend(container, marker);
|
||||
effect(function() { return (function() {
|
||||
var parent = domParent(marker);
|
||||
return (isSxTruthy(parent) ? (domRemoveChildrenAfter(marker), (function() {
|
||||
var items = deref(itemsSig);
|
||||
return (isSxTruthy(parent) ? (function() {
|
||||
var newMap = {};
|
||||
var newKeys = [];
|
||||
var hasKeys = false;
|
||||
forEachIndexed(function(idx, item) { return (function() {
|
||||
var rendered = renderListItem(mapFn, item, env, ns);
|
||||
var key = extractKey(rendered, idx);
|
||||
if (isSxTruthy((isSxTruthy(!isSxTruthy(hasKeys)) && !isSxTruthy(startsWith(key, "__idx_"))))) {
|
||||
hasKeys = true;
|
||||
}
|
||||
(isSxTruthy(dictHas(keyMap, key)) ? dictSet(newMap, key, dictGet(keyMap, key)) : dictSet(newMap, key, rendered));
|
||||
return append_b(newKeys, key);
|
||||
})(); }, items);
|
||||
(isSxTruthy(!isSxTruthy(hasKeys)) ? (domRemoveChildrenAfter(marker), (function() {
|
||||
var frag = createFragment();
|
||||
{ var _c = items; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (function() {
|
||||
var rendered = (isSxTruthy(isLambda(mapFn)) ? renderLambdaDom(mapFn, [item], env, ns) : renderToDom(apply(mapFn, [item]), env, ns));
|
||||
return domAppend(frag, rendered);
|
||||
})(); } }
|
||||
{ var _c = newKeys; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; domAppend(frag, dictGet(newMap, k)); } }
|
||||
return domInsertAfter(marker, frag);
|
||||
})()) : NIL);
|
||||
})()) : (forEach(function(oldKey) { return (isSxTruthy(!isSxTruthy(dictHas(newMap, oldKey))) ? domRemove(dictGet(keyMap, oldKey)) : NIL); }, keyOrder), (function() {
|
||||
var cursor = marker;
|
||||
return forEach(function(k) { return (function() {
|
||||
var node = dictGet(newMap, k);
|
||||
var next = domNextSibling(cursor);
|
||||
if (isSxTruthy(!isSxTruthy(isIdentical(node, next)))) {
|
||||
domInsertAfter(cursor, node);
|
||||
}
|
||||
return (cursor = node);
|
||||
})(); }, newKeys);
|
||||
})()));
|
||||
keyMap = newMap;
|
||||
return (keyOrder = newKeys);
|
||||
})() : NIL);
|
||||
})(); });
|
||||
return container;
|
||||
})(); };
|
||||
|
||||
Reference in New Issue
Block a user