vanjs-ext
Version:
The official extension library for VanJS
153 lines (130 loc) • 5.65 kB
JavaScript
import van from "vanjs-core"
// This file consistently uses `let` keyword instead of `const` for reducing the bundle size.
// Global variables - aliasing some builtin symbols to reduce the bundle size.
let {fromEntries, entries, keys, hasOwn, getPrototypeOf, create, assign} = Object
let {get: refGet, set: refSet, deleteProperty: refDelete, ownKeys: refOwnKeys} = Reflect
let {state, derive, add} = van
let statesToGc, gcCycleInMs = 1000, _undefined, replacing
let statesSym = Symbol(), isCalcFunc = Symbol(), bindingsSym = Symbol(), keysGenSym = Symbol(), keyToChildSym = Symbol(), noreactiveSym = Symbol()
let calc = f => (f[isCalcFunc] = 1, f)
let isObject = x => x instanceof Object && !(x instanceof Function) && !x[noreactiveSym]
let toState = v => {
if (v?.[isCalcFunc]) {
let s = state()
derive(() => {
let newV = v()
isObject(s.rawVal) && isObject(newV) ? replace(s.rawVal, newV) : s.val = reactive(newV)
})
return s
} else return state(reactive(v))
}
let buildStates = srcObj => {
let states = Array.isArray(srcObj) ? [] : {__proto__: getPrototypeOf(srcObj)}
for (let [k, v] of entries(srcObj)) states[k] = toState(v)
states[bindingsSym] = []
states[keysGenSym] = state(1)
return states
}
let reactiveHandler = {
get: (states, name, proxy) =>
name === statesSym ? states :
hasOwn(states, name) ?
Array.isArray(states) && name === "length" ?
(states[keysGenSym].val, states.length) :
states[name].val :
refGet(states, name, proxy),
set: (states, name, v, proxy) =>
hasOwn(states, name) ?
Array.isArray(states) && name === "length" ?
(v !== states.length && ++states[keysGenSym].val, states.length = v, 1) :
(states[name].val = reactive(v), 1) :
name in states ? refSet(states, name, v, proxy) :
refSet(states, name, toState(v)) && (
++states[keysGenSym].val,
filterBindings(states).forEach(
addToContainer.bind(_undefined, proxy, name, states[name], replacing)),
1
),
deleteProperty: (states, name) =>
(refDelete(states, name) && onDelete(states, name), ++states[keysGenSym].val),
ownKeys: states => (states[keysGenSym].val, refOwnKeys(states)),
}
let reactive = srcObj => !isObject(srcObj) || srcObj[statesSym] ? srcObj :
new Proxy(buildStates(srcObj), reactiveHandler)
let noreactive = x => (x[noreactiveSym] = 1, x)
let stateFields = obj => obj[statesSym]
let stateProto = getPrototypeOf(state())
let rawStates = states => new Proxy(states, {
get: (states, name, proxy) => getPrototypeOf(states[name] ?? 0) === stateProto ?
{val: raw(states[name].rawVal)} : refGet(states, name, proxy),
})
let raw = obj => obj?.[statesSym] ? new Proxy(rawStates(obj[statesSym]), reactiveHandler) : obj
let filterBindings = states =>
states[bindingsSym] = states[bindingsSym].filter(b => b._containerDom.isConnected)
let addToContainer = (items, k, v, skipReorder, {_containerDom, f}) => {
let isArray = Array.isArray(items), typedK = isArray ? Number(k) : k
add(_containerDom, () =>
_containerDom[keyToChildSym][k] = f(v, () => delete items[k], typedK))
isArray && !skipReorder && typedK !== items.length - 1 &&
_containerDom.insertBefore(_containerDom.lastChild,
_containerDom[keyToChildSym][keys(items).find(key => Number(key) > typedK)])
}
let onDelete = (states, k) => {
for (let b of filterBindings(states)) {
let keyToChild = b._containerDom[keyToChildSym]
keyToChild[k]?.remove()
delete keyToChild[k]
}
}
let addStatesToGc = states => (statesToGc ?? (statesToGc = (
setTimeout(
() => (statesToGc.forEach(filterBindings), statesToGc = _undefined), gcCycleInMs),
new Set))).add(states)
let list = (container, items, itemFunc) => {
let binding = {_containerDom: container instanceof Function ? container() : container, f: itemFunc}
let states = items[statesSym]
binding._containerDom[keyToChildSym] = {}
states[bindingsSym].push(binding)
addStatesToGc(states)
for (let [k, v] of entries(states)) addToContainer(items, k, v, 1, binding)
return binding._containerDom
}
let replaceInternal = (obj, replacement) => {
for (let [k, v] of entries(replacement)) {
let existingV = obj[k]
isObject(existingV) && isObject(v) ? replaceInternal(existingV, v) : obj[k] = v
}
for (let k in obj) hasOwn(replacement, k) || delete obj[k]
let newKeys = keys(replacement), isArray = Array.isArray(obj)
if (isArray || keys(obj).some((k, i) => k !== newKeys[i])) {
let states = obj[statesSym]
if (isArray) obj.length = replacement.length; else {
++states[keysGenSym].val
let statesCopy = {...states}
for (let k of newKeys) delete states[k]
for (let k of newKeys) states[k] = statesCopy[k]
}
for (let {_containerDom} of filterBindings(states)) {
let {firstChild: dom, [keyToChildSym]: keyToChild} = _containerDom
for (let k of newKeys) dom === keyToChild[k] ?
dom = dom.nextSibling : _containerDom.insertBefore(keyToChild[k], dom)
}
}
return obj
}
let replace = (obj, replacement) => {
replacing = 1
try {
return replaceInternal(obj, replacement instanceof Function ?
Array.isArray(obj) ? replacement(obj.filter(_ => 1)) : fromEntries(replacement(entries(obj))) :
replacement
)
} finally {
replacing = _undefined
}
}
let compact = obj => Array.isArray(obj) ? obj.filter(_ => 1).map(compact) :
isObject(obj) ?
assign(create(getPrototypeOf(obj)), fromEntries(entries(obj).map(([k, v]) => [k, compact(v)])))
: obj
export {calc, reactive, noreactive, stateFields, raw, list, replace, compact}