refui
Version:
The JavaScript framework that refuels your UI projects, across web, native, and embedded
559 lines (472 loc) • 12.8 kB
JavaScript
import { collectDisposers, tick, nextTick, read, peek, watch, onDispose, freeze, signal, isSignal } from './signal.js'
import { nop, removeFromArr, isThenable, isPrimitive } from './utils.js'
import { createHMRComponentWrap } from './hmr.js'
const KEY_CTX = Symbol(process.env.NODE_ENV === 'production' ? '' : 'K_Ctx')
let currentCtx = null
function _captured(capturedCtx, ...args) {
const prevCtx = currentCtx
currentCtx = capturedCtx
try {
return this(...args)
} finally {
currentCtx = prevCtx
}
}
function capture(fn) {
return _captured.bind(freeze(fn), currentCtx)
}
function _runInSnapshot(fn, ...args) {
return fn(...args)
}
function snapshot() {
return capture(_runInSnapshot)
}
function exposeReducer(descriptors, [key, value]) {
if (isSignal(value)) {
descriptors[key] = {
get: value.get.bind(value),
set: value.set.bind(value),
enumerable: true,
configurable: true
}
} else {
descriptors[key] = {
value,
enumerable: true,
configurable: true
}
}
return descriptors
}
function expose(kvObj) {
if (!currentCtx || isPrimitive(kvObj)) {
return
}
const entries = Object.entries(kvObj)
if (entries.length) {
currentCtx.hasExpose = true
const descriptors = entries.reduce(exposeReducer, {})
Object.defineProperties(currentCtx.self, descriptors)
if (currentCtx.wrapper) {
Object.defineProperties(currentCtx.wrapper, descriptors)
}
}
}
function render(instance, renderer) {
const ctx = instance[KEY_CTX]
if (!ctx) {
return
}
const { run, render: renderComponent } = ctx
if (!renderComponent || typeof renderComponent !== 'function') return renderComponent
return run(renderComponent, renderer)[0]
}
function dispose(instance) {
const ctx = instance[KEY_CTX]
if (!ctx) {
return
}
ctx.dispose()
}
function getCurrentSelf() {
return currentCtx?.self
}
function Fn({ name = 'Fn', ctx }, handler, handleError) {
if (!handler) {
return nop
}
const run = currentCtx?.run
if (!run) {
return nop
}
return function(R) {
const fragment = R.createFragment(name)
let currentRender = null
let currentDispose = null
watch(function() {
const newRender = read(handler(read(ctx)))
if (newRender === currentRender) return
currentRender = newRender
if (newRender) {
const prevDispose = currentDispose
currentDispose = run(function() {
let newResult = null
let errored = false
try {
newResult = R.ensureElement((typeof newRender === 'function') ? newRender(R) : newRender)
} catch (err) {
errored = true
const errorHandler = peek(handleError)
if (errorHandler) {
newResult = R.ensureElement(errorHandler(err, name, ctx))
} else {
throw err
}
}
if (!errored && prevDispose) {
prevDispose()
}
if (newResult) {
R.appendNode(fragment, newResult)
onDispose(nextTick.bind(null, R.removeNode.bind(null, newResult)))
} else {
if (errored) {
onDispose(prevDispose)
}
}
})[1]
} else {
currentDispose?.()
currentDispose = null
}
})
return fragment
}
}
function For({ name = 'For', entries, track, indexed }, item) {
let currentData = []
let kv = track && new Map()
let ks = indexed && new Map()
let nodeCache = new Map()
let disposers = new Map()
function _clear() {
for (let [, _dispose] of disposers) _dispose(true)
nodeCache = new Map()
disposers = new Map()
if (ks) ks = new Map()
}
function flushKS() {
if (ks) {
for (let i = 0; i < currentData.length; i++) {
const sig = ks.get(currentData[i])
sig.value = i
}
}
}
function getItem(itemKey) {
return (kv ? kv.get(itemKey) : itemKey)
}
function remove(itemKey) {
const itemData = getItem(itemKey)
removeFromArr(peek(entries), itemData)
entries.trigger()
}
function clear() {
if (!currentData.length) return
_clear()
if (kv) kv = new Map()
currentData = []
if (entries.value.length) entries.value = []
}
onDispose(_clear)
expose({
getItem,
remove,
clear
})
return function(R) {
const fragment = R.createFragment(name)
function getItemNode(itemKey) {
let node = nodeCache.get(itemKey)
if (!node) {
const newDataItem = kv ? kv.get(itemKey) : itemKey
let idxSig = ks ? ks.get(itemKey) : 0
if (ks && !idxSig) {
idxSig = signal(0)
ks.set(itemKey, idxSig)
}
const dispose = collectDisposers(
[],
function() {
node = item(newDataItem, idxSig, R)
nodeCache.set(itemKey, node)
},
function(batch) {
if (!batch) {
nodeCache.delete(itemKey)
disposers.delete(itemKey)
if (ks) ks.delete(itemKey)
if (kv) kv.delete(itemKey)
}
if (node) R.removeNode(node)
}
)
disposers.set(itemKey, dispose)
}
return node
}
// eslint-disable-next-line complexity
watch(function() {
/* eslint-disable max-depth */
const data = read(entries)
if (!data || !data.length) return clear()
let oldData = currentData
if (track) {
kv = new Map()
const key = read(track)
currentData = data.map(function(i) {
const itemKey = i[key]
kv.set(itemKey, i)
return itemKey
})
} else currentData = [...data]
let newData = null
if (oldData.length) {
const obsoleteDataKeys = [...new Set([...currentData, ...oldData])].slice(currentData.length)
if (obsoleteDataKeys.length === oldData.length) {
_clear()
newData = currentData
} else {
if (obsoleteDataKeys.length) {
for (let oldItemKey of obsoleteDataKeys) {
disposers.get(oldItemKey)()
removeFromArr(oldData, oldItemKey)
}
}
const newDataKeys = [...new Set([...oldData, ...currentData])].slice(oldData.length)
const hasNewKeys = !!newDataKeys.length
let newDataCursor = 0
while (newDataCursor < currentData.length) {
if (!oldData.length) {
if (newDataCursor) newData = currentData.slice(newDataCursor)
break
}
const frontSet = []
const backSet = []
let frontChunk = []
let backChunk = []
let prevChunk = frontChunk
let oldDataCursor = 0
let oldItemKey = oldData[0]
let newItemKey = currentData[newDataCursor]
while (oldDataCursor < oldData.length) {
const isNewKey = hasNewKeys && newDataKeys.includes(newItemKey)
if (isNewKey || oldItemKey === newItemKey) {
if (prevChunk !== frontChunk) {
backSet.push(backChunk)
backChunk = []
prevChunk = frontChunk
}
frontChunk.push(newItemKey)
if (isNewKey) {
R.insertBefore(getItemNode(newItemKey), getItemNode(oldItemKey))
} else {
oldDataCursor += 1
oldItemKey = oldData[oldDataCursor]
}
newDataCursor += 1
newItemKey = currentData[newDataCursor]
} else {
if (prevChunk !== backChunk) {
frontSet.push(frontChunk)
frontChunk = []
prevChunk = backChunk
}
backChunk.push(oldItemKey)
oldDataCursor += 1
oldItemKey = oldData[oldDataCursor]
}
}
if (prevChunk === frontChunk) {
frontSet.push(frontChunk)
}
backSet.push(backChunk)
frontSet.shift()
for (let i = 0; i < frontSet.length; i++) {
const fChunk = frontSet[i]
const bChunk = backSet[i]
if (fChunk.length <= bChunk.length) {
const beforeAnchor = getItemNode(bChunk[0])
backSet[i + 1] = bChunk.concat(backSet[i + 1])
bChunk.length = 0
for (let itemKey of fChunk) {
R.insertBefore(getItemNode(itemKey), beforeAnchor)
}
} else if (backSet[i + 1].length) {
const beforeAnchor = getItemNode(backSet[i + 1][0])
for (let itemKey of bChunk) {
R.insertBefore(getItemNode(itemKey), beforeAnchor)
}
} else {
R.appendNode(fragment, ...bChunk.map(getItemNode))
}
}
oldData = [].concat(...backSet)
}
}
} else {
newData = currentData
}
if (newData) {
for (let newItemKey of newData) {
const node = getItemNode(newItemKey)
if (node) R.appendNode(fragment, node)
}
}
flushKS()
})
return fragment
}
}
function If({ condition, else: otherwise }, trueBranch, falseBranch) {
if (otherwise) {
falseBranch = otherwise
}
if (isSignal(condition)) {
return Fn({ name: 'If' }, function() {
if (condition.value) return trueBranch
else return falseBranch
})
}
if (typeof condition === 'function') {
return Fn({ name: 'If' }, function() {
if (condition()) {
return trueBranch
} else {
return falseBranch
}
})
}
if (condition) return trueBranch
return falseBranch
}
function _dynContainer(name, catchErr, ctx, props, ...children) {
const self = currentCtx.self
const $ref = props.$ref ??= signal()
let current = null
let renderFn = null
return Fn({ name, ctx }, () => {
const component = read(this)
if (current === component) {
return renderFn
}
if (component === undefined || component === null) {
return (current = renderFn = null)
}
current = component
renderFn = function(R) {
const ret = R.c(component, props, ...children)
const newInstance = $ref.peek()
const newCtx = newInstance?.[KEY_CTX]
if (newCtx) {
if (newCtx.hasExpose) {
const extraKeys = Object.getOwnPropertyDescriptors(newInstance)
delete extraKeys[KEY_CTX]
Object.defineProperties(self, extraKeys)
}
newCtx.wrapper = self
}
return ret
}
return renderFn
}, catchErr)
}
function Dynamic({ is, ctx, ...props }, ...children) {
return _dynContainer.call(is, 'Dynamic', null, ctx, props, ...children)
}
function Async({ future, fallback }) {
const self = getCurrentSelf()
const component = signal(fallback)
Promise.resolve(future).then(capture(function(result) {
if (self[KEY_CTX]) {
watch(function() {
component.value = read(result)
})
}
}))
return Fn({ name: 'Async' }, function() {
return component.value
})
}
function Render({ from }) {
return function(R) {
return R.c(Fn, { name: 'Render' }, function() {
const instance = read(from)
if (instance !== null && instance !== undefined) return render(instance, R)
})
}
}
class Component {
constructor(tpl, props, ...children) {
const ctx = {
run: null,
render: null,
dispose: null,
wrapper: null,
hasExpose: false,
self: this
}
const prevCtx = currentCtx
currentCtx = ctx
const disposers = []
ctx.run = capture(function(fn, ...args) {
let result
const cleanup = collectDisposers([], function() {
result = fn(...args)
}, function(batch) {
if (!batch) {
removeFromArr(disposers, cleanup)
}
})
disposers.push(cleanup)
return [result, cleanup]
})
try {
ctx.dispose = collectDisposers(disposers, function() {
let renderFn = tpl(props, ...children)
if (isThenable(renderFn)) {
renderFn = Async({future: renderFn, fallback: props && props.fallback || null})
}
ctx.render = renderFn
}, () => {
Object.defineProperty(this, KEY_CTX, {
value: null,
enumerable: false
})
})
} catch (error) {
for (let i of disposers) i(true)
throw error
} finally {
currentCtx = prevCtx
}
Object.defineProperty(this, KEY_CTX, {
value: ctx,
enumerable: false,
configurable: true
})
}
}
const createComponent = (function() {
function createComponentRaw(tpl, props, ...children) {
props ??= {}
if (isSignal(tpl)) {
return new Component(_dynContainer.bind(tpl, 'Signal', null, null), props, ...children)
}
const { $ref, ..._props } = props
const component = new Component(tpl, _props, ...children)
if ($ref) $ref.value = component
return component
}
if (import.meta.hot) {
const builtins = new WeakSet([Fn, For, If, Dynamic, Async, Render, Component])
return createHMRComponentWrap({ builtins, _dynContainer, Component, createComponentRaw })
}
return createComponentRaw
})()
export {
capture,
snapshot,
expose,
render,
dispose,
getCurrentSelf,
Fn,
For,
If,
Dynamic,
Async,
Render,
Component,
createComponent
}