sprae
Version:
DOM microhydration
86 lines (67 loc) • 2.92 kB
JavaScript
import sprae, { _state, dir, frag, parse } from "../core.js";
import store, { _change, _signals } from "../store.js";
import { effect } from '../signal.js';
dir('each', (tpl, state, expr) => {
let [itemVar, idxVar = "$"] = expr.split(/\bin\b/)[0].trim().split(/\s*,\s*/);
// we need :if to be able to replace holder instead of tpl for :if :each case
let holder = document.createTextNode("");
// we re-create items any time new items are produced
let cur, keys, items, prevl = 0
let update = () => {
let i = 0, newItems = items, newl = newItems.length
// plain array update, not store (signal with array) - updates full list
if (cur && !cur[_change]) {
for (let s of cur[_signals] || []) s[Symbol.dispose]()
cur = null, prevl = 0
}
// delete
if (newl < prevl) cur.length = newl
// update, append, init
else {
// init
if (!cur) cur = newItems
// update
else while (i < prevl) cur[i] = newItems[i++]
// append
for (; i < newl; i++) {
cur[i] = newItems[i]
let idx = i,
// FIXME: inherited state is cheaper in terms of memory and faster in terms of performance
// compared to cloning all parent signals and creating a proxy
// FIXME: besides try to avoid _signals access: we can optimize store then not checking for _signals key
scope = store({
[itemVar]: cur[_signals]?.[idx] || cur[idx],
[idxVar]: keys ? keys[idx] : idx
}, state),
el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
holder.before(el.content || el);
sprae(el, scope);
// signal/holder disposal removes element
((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose] = () => {
el[Symbol.dispose]?.(), el.remove()
};
}
}
prevl = newl
}
tpl.replaceWith(holder);
tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
return value => {
// obtain new items
keys = null
if (typeof value === "number") items = Array.from({ length: value }, (_, i) => i + 1)
else if (value?.constructor === Object) keys = Object.keys(value), items = Object.values(value)
else items = value || []
// whenever list changes, we rebind internal change effect
let planned = 0
return effect(() => {
// subscribe to items change (.length) - we do it every time (not just in update) since preact unsubscribes unused signals
items[_change]?.value
// make first render immediately, debounce subsequent renders
if (!planned++) update(), queueMicrotask(() => (planned > 1 && update(), planned = 0));
})
}
},
// redefine evaluator to take second part of expression
expr => parse(expr.split(/\bin\b/)[1])
)