hyperapp
Version:
1 KB JavaScript library for building frontend applications.
277 lines (231 loc) • 7 kB
JavaScript
var globalInvokeLaterStack = []
export function app(props) {
var appState
var appView = props.view
var appActions = {}
var appEvents = {}
var appMixins = props.mixins || []
var appRoot = props.root || document.body
var element
var oldNode
var renderLock
appMixins.concat(props).map(function(mixin) {
mixin = typeof mixin === "function" ? mixin(emit) : mixin
Object.keys(mixin.events || []).map(function(key) {
appEvents[key] = (appEvents[key] || []).concat(mixin.events[key])
})
appState = merge(appState, mixin.state)
initialize(appActions, mixin.actions)
})
requestRender(
(oldNode = emit("load", (element = appRoot.children[0]))) === element &&
(oldNode = element = null)
)
return emit
function initialize(actions, withActions, lastName) {
Object.keys(withActions || []).map(function(key) {
var action = withActions[key]
var name = lastName ? lastName + "." + key : key
if (typeof action === "function") {
actions[key] = function(data) {
emit("action", { name: name, data: data })
var result = emit("resolve", action(appState, appActions, data))
return typeof result === "function" ? result(update) : update(result)
}
} else {
initialize(actions[key] || (actions[key] = {}), action, name)
}
})
}
function render(cb) {
element = patch(
appRoot,
element,
oldNode,
(oldNode = emit("render", appView)(appState, appActions)),
(renderLock = !renderLock)
)
while ((cb = globalInvokeLaterStack.pop())) cb()
}
function requestRender() {
if (appView && !renderLock) {
requestAnimationFrame(render, (renderLock = !renderLock))
}
}
function update(withState) {
if (typeof withState === "function") {
return update(withState(appState))
}
if (withState && (withState = emit("update", merge(appState, withState)))) {
requestRender((appState = withState))
}
return appState
}
function emit(name, data) {
return (
(appEvents[name] || []).map(function(cb) {
var result = cb(appState, appActions, data)
if (result != null) {
data = result
}
}),
data
)
}
function merge(a, b) {
var obj = {}
for (var i in a) {
obj[i] = a[i]
}
for (var i in b) {
obj[i] = b[i]
}
return obj
}
function getKey(node) {
if (node && (node = node.data)) {
return node.key
}
}
function createElement(node, isSVG) {
if (typeof node === "string") {
var element = document.createTextNode(node)
} else {
var element = (isSVG = isSVG || node.tag === "svg")
? document.createElementNS("http://www.w3.org/2000/svg", node.tag)
: document.createElement(node.tag)
if (node.data && node.data.oncreate) {
globalInvokeLaterStack.push(function() {
node.data.oncreate(element)
})
}
for (var i in node.data) {
setData(element, i, node.data[i])
}
for (var i = 0; i < node.children.length; ) {
element.appendChild(createElement(node.children[i++], isSVG))
}
}
return element
}
function setData(element, name, value, oldValue) {
if (name === "key") {
} else if (name === "style") {
for (var i in merge(oldValue, (value = value || {}))) {
element.style[i] = value[i] || ""
}
} else {
try {
element[name] = value
} catch (_) {}
if (typeof value !== "function") {
if (value) {
element.setAttribute(name, value)
} else {
element.removeAttribute(name)
}
}
}
}
function updateElement(element, oldData, data) {
for (var i in merge(oldData, data)) {
var value = data[i]
var oldValue = i === "value" || i === "checked" ? element[i] : oldData[i]
if (value !== oldValue) {
setData(element, i, value, oldValue)
}
}
if (data && data.onupdate) {
globalInvokeLaterStack.push(function() {
data.onupdate(element, oldData)
})
}
}
function removeElement(parent, element, data) {
if (data && data.onremove) {
data.onremove(element)
} else {
parent.removeChild(element)
}
}
function patch(parent, element, oldNode, node, isSVG, nextSibling) {
if (oldNode == null) {
element = parent.insertBefore(createElement(node, isSVG), element)
} else if (node.tag != null && node.tag === oldNode.tag) {
updateElement(element, oldNode.data, node.data)
isSVG = isSVG || node.tag === "svg"
var len = node.children.length
var oldLen = oldNode.children.length
var oldKeyed = {}
var oldElements = []
var keyed = {}
for (var i = 0; i < oldLen; i++) {
var oldElement = (oldElements[i] = element.childNodes[i])
var oldChild = oldNode.children[i]
var oldKey = getKey(oldChild)
if (null != oldKey) {
oldKeyed[oldKey] = [oldElement, oldChild]
}
}
var i = 0
var j = 0
while (j < len) {
var oldElement = oldElements[i]
var oldChild = oldNode.children[i]
var newChild = node.children[j]
var oldKey = getKey(oldChild)
if (keyed[oldKey]) {
i++
continue
}
var newKey = getKey(newChild)
var keyedNode = oldKeyed[newKey] || []
if (null == newKey) {
if (null == oldKey) {
patch(element, oldElement, oldChild, newChild, isSVG)
j++
}
i++
} else {
if (oldKey === newKey) {
patch(element, keyedNode[0], keyedNode[1], newChild, isSVG)
i++
} else if (keyedNode[0]) {
element.insertBefore(keyedNode[0], oldElement)
patch(element, keyedNode[0], keyedNode[1], newChild, isSVG)
} else {
patch(element, oldElement, null, newChild, isSVG)
}
j++
keyed[newKey] = newChild
}
}
while (i < oldLen) {
var oldChild = oldNode.children[i]
var oldKey = getKey(oldChild)
if (null == oldKey) {
removeElement(element, oldElements[i], oldChild.data)
}
i++
}
for (var i in oldKeyed) {
var keyedNode = oldKeyed[i]
var reusableNode = keyedNode[1]
if (!keyed[reusableNode.data.key]) {
removeElement(element, keyedNode[0], reusableNode.data)
}
}
} else if (element && node !== element.nodeValue) {
if (typeof node === "string" && typeof oldNode === "string") {
element.nodeValue = node
} else {
element = parent.insertBefore(
createElement(node, isSVG),
(nextSibling = element)
)
removeElement(parent, nextSibling, oldNode.data)
}
}
return element
}
}