Skip to content

Instantly share code, notes, and snippets.

@Pyrolistical
Last active October 27, 2017 06:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pyrolistical/f3a4b1577741dd847aef73300f8f11c3 to your computer and use it in GitHub Desktop.
Save Pyrolistical/f3a4b1577741dd847aef73300f8f11c3 to your computer and use it in GitHub Desktop.
Hyperapp views PoC
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hyperapp Views</title>
</head>
<body>
<div id="app-entry"></div>
<script src="https://unpkg.com/hyperapp@0.15.1/dist/hyperapp.js"></script>
<script>
const { h } = hyperapp
// can be in its own file counter.js
const counter = {
state: {
count: 0
},
view(state, actions) {
return h("div", {}, [
h("h2", {}, `${state.name} ${state.count}`),
h("button", {
onclick: actions.down,
disabled: state.count <= 0
}, "–"),
h("button", {
onclick: actions.up
}, "+")
])
},
actions: {
down(state) {
return {
count: state.count - 1
}
},
up(state) {
return {
count: state.count + 1
}
}
}
}
// withViews + injectViews can be in its own file with-views.js
function injectViews(props) {
const injectedSubmodules = {}
for (const m in props.modules) {
injectedSubmodules[m] = injectViews(props.modules[m])
}
return {
...props,
view(state, actions) {
const views = {}
for (const m in injectedSubmodules) {
views[m] = (props) => {
return injectedSubmodules[m].view({
...state[m],
...props
}, actions[m])
}
}
return props.view(state, actions, views)
},
modules: injectedSubmodules
}
}
function withViews(app) {
return (props, container) => {
return app(injectViews(props), container)
}
}
function many(module) {
return {
state: {
substates: []
},
view(state, actions) {
return state.substates.map((substate, i) => {
const subactions = {}
for (const action in module.actions) {
subactions[action] = () => {
const updatedSubstate = module.actions[action](substate)
actions.update({
substates: [...state.substates.slice(0, i), updatedSubstate, ...state.substates.slice(i + 1, state.substates.length)]
})
}
}
return module.view({
...substate,
...state.extraProps(i)
}, subactions)
})
},
actions: {
push(state, actions) {
return {
substates: [...state.substates, {
...module.state
}]
}
},
update(state, actions, data) {
return data
}
}
}
}
withViews(app)({
view(state, actions, {aleft, aright, bleft, bright, counters}) {
return h("main", {}, [
h("h1", {}, "A. Two counters with sum"),
h(aleft, {name: "Left"}),
h(aright, {name: "Right"}),
h("p", {}, `Sum ${state.aleft.count + state.aright.count}`),
h("h1", {}, "B. Two counters with sum and add both"),
h(bleft, {name: "Left"}),
h(bright, {name: "Right"}),
h("button", {
onclick: actions.addBoth
}, "Both"),
h("p", {}, `Sum ${state.bleft.count + state.bright.count}`),
h("h1", {}, "C. Counters, counters everywhere"),
h(counters, {extraProps: (index) => ({name: `counter[${index}]`})}),
h("button", {
onclick: actions.counters.push
}, "Add Counter"),
h("p", {}, `Sum ${state.counters.substates.reduce((total, {count}) => total + count, 0)}`),
])
},
actions: {
addBoth(state, actions) {
actions.bleft.up()
actions.bright.up()
// this will work after https://github.com/hyperapp/hyperapp/pull/426
// return {
// bleft: {
// ...state.bleft, // not needed in this case, but generally is
// count: state.bleft.count + 1
// },
// bright: {
// ...state.bright, // not needed in this case, but generally is
// count: state.bright.count + 1
// }
// }
}
},
modules: {
aleft: counter,
aright: counter,
bleft: counter,
bright: counter,
counters: many(counter)
}
}, document.getElementById('app-entry'))
// using https://github.com/hyperapp/hyperapp/pull/426
// b7b4645859be0052d1fb3d45605e407726ef83b6
function app(props, container) {
var root = (container = container || document.body).children[0]
var node = toVNode(root, [].map)
var callbacks = []
var skipRender
var globalState
var globalActions
repaint(
flush(
initModule(
props,
(globalState = {}),
(globalActions = {}),
updateGlobalState,
function() {
return globalState
}
)
)
)
return globalActions
function repaint() {
if (props.view && !skipRender) {
requestAnimationFrame(render, (skipRender = !skipRender))
}
}
function render() {
flush(
(root = patchElement(
container,
root,
node,
(node = props.view(globalState, globalActions)),
(skipRender = !skipRender)
))
)
}
function flush(cb) {
while ((cb = callbacks.pop())) cb()
}
function toVNode(element, map) {
return (
element &&
h(
element.tagName.toLowerCase(),
{},
map.call(element.childNodes, function(element) {
return element.nodeType === 3
? element.nodeValue
: toVNode(element, map)
})
)
)
}
/**
* Initializes the given module:
* - computes the initial state
* - initalize all actions
* - add module.init() to be called before the first render
* - initialize sub-modules
*
* @param module the module to initialize
* @param state the initial state (updated by this function)
* @param actions the actions object (updated by this function)
* @param update the update() function for the current state slice
* @param getState function () => state that returns the current (up-to-date) state slice
*/
function initModule(module, state, actions, update, getState) {
if (module.init) {
callbacks.push(function() {
module.init(state, actions)
})
}
assign(state, module.state)
initModuleActions(state, actions, module.actions, update, getState)
for (var key in module.modules) {
initModule(
module.modules[key],
// do not override state is already exist in current module
state[key] || (state[key] = {}),
// do not override actions is already exist in current module
actions[key] || (actions[key] = {}),
updateFor(update, getState, key),
getStateFor(getState, key)
)
}
}
/**
* Initializes the given actions:
* - bind the moduleActions to the current state slice/actions
* - set state = {} for children actions, if needed
* - recursively initialize children actions
*
* @param moduleActions the current module's actions, contains actions and other action objects
* @param state the initial state object, this is passed here to avoid undefined state when
* computing and action's state slice
* @param actions the initalized actions object (updated by this function)
* @param update the update() function for the actions' relevant state slices
* @param getState function: () => state that return the relevant state slice for the actions
*/
function initModuleActions(state, actions, moduleActions, update, getState) {
Object.keys(moduleActions || {}).map(function(key) {
if (typeof moduleActions[key] === "function") {
actions[key] = function(data) {
return typeof (data = moduleActions[key](
getState(),
actions,
data
)) === "function"
? data(update)
: update(data)
}
} else {
initModuleActions(
state[key] || (state[key] = {}),
(actions[key] = {}),
moduleActions[key],
updateFor(update, getState, key),
getStateFor(getState, key)
)
}
})
}
/**
* Merge the global state with the given result and triggers a repaint.
* This is the update() function for the app's prop.
* @param result the result to merge it, a function (globalState) => result, or falsy to not trigger repaint
* @returns globalState
*/
function updateGlobalState(result) {
return (
typeof result === "function"
? updateGlobalState(result(globalState))
: result && repaint((globalState = merge(globalState, result))),
globalState
)
}
/**
* Wraps the given update() function and return a new update() that can be used for the state slice getState()[prop].
*
* @param update the update() function to wrap
* @param getState function: () => state that returns the parrent's state slice
* (getState()[prop] contains the current state slice)
* @param prop the property of the state slice to create the update() function for
* @param parentResult internal variable added to avoid declaration (saves 1 "var "), should not be set
*
* @returns the new update function specialized for the given state slice
*/
function updateFor(update, getState, prop, parentResult) {
return function(result) {
;(parentResult = {})[prop] = merge(
getState()[prop],
typeof result === "function" ? result(getState()[prop]) : result
)
return update(parentResult)[prop]
}
}
/**
* Function: (getState: () => state, prop: string) => () => state[prop].
*
* @param getState the getter for the state: () => state that gets wrapped
* @param prop the state slice to get
* @returns a new state getter function: () => getState()[prop]
*/
function getStateFor(getState, prop) {
return function() {
return getState()[prop]
}
}
function assign(target, source) {
for (var i in source) {
target[i] = source[i]
}
return target
}
function merge(target, source) {
return assign(assign({}, target), source)
}
function createElement(node, isSVG) {
if (typeof node === "string") {
var element = document.createTextNode(node)
} else {
var element = (isSVG = isSVG || node.type === "svg")
? document.createElementNS("http://www.w3.org/2000/svg", node.type)
: document.createElement(node.type)
if (node.props && node.props.oncreate) {
callbacks.push(function() {
node.props.oncreate(element)
})
}
for (var i = 0; i < node.children.length; i++) {
element.appendChild(createElement(node.children[i], isSVG))
}
for (var i in node.props) {
setElementProp(element, i, node.props[i])
}
}
return element
}
function setElementProp(element, name, value, oldValue) {
if (name === "key") {
} else if (name === "style") {
for (var name in merge(oldValue, (value = value || {}))) {
element.style[name] = value[name] || ""
}
} else {
try {
element[name] = value
} catch (_) {}
if (typeof value !== "function") {
if (value) {
element.setAttribute(name, value)
} else {
element.removeAttribute(name)
}
}
}
}
function updateElement(element, oldProps, props) {
for (var i in merge(oldProps, props)) {
var value = props[i]
var oldValue = i === "value" || i === "checked" ? element[i] : oldProps[i]
if (value !== oldValue) {
value !== oldValue && setElementProp(element, i, value, oldValue)
}
}
if (props && props.onupdate) {
callbacks.push(function() {
props.onupdate(element, oldProps)
})
}
}
function removeElement(parent, element, props) {
if (
props &&
props.onremove &&
typeof (props = props.onremove(element)) === "function"
) {
props(remove)
} else {
remove()
}
function remove() {
parent.removeChild(element)
}
}
function getKey(node) {
if (node && node.props) {
return node.props.key
}
}
function patchElement(parent, element, oldNode, node, isSVG, nextSibling) {
if (oldNode == null) {
element = parent.insertBefore(createElement(node, isSVG), element)
} else if (node.type != null && node.type === oldNode.type) {
updateElement(element, oldNode.props, node.props)
isSVG = isSVG || node.type === "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) {
patchElement(element, oldElement, oldChild, newChild, isSVG)
j++
}
i++
} else {
if (oldKey === newKey) {
patchElement(element, keyedNode[0], keyedNode[1], newChild, isSVG)
i++
} else if (keyedNode[0]) {
element.insertBefore(keyedNode[0], oldElement)
patchElement(element, keyedNode[0], keyedNode[1], newChild, isSVG)
} else {
patchElement(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.props)
}
i++
}
for (var i in oldKeyed) {
var keyedNode = oldKeyed[i]
var reusableNode = keyedNode[1]
if (!keyed[reusableNode.props.key]) {
removeElement(element, keyedNode[0], reusableNode.props)
}
}
} 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.props)
}
}
return element
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment