Last active
August 31, 2017 02:39
-
-
Save tscholl2/a264e43eb3a4185c2ebcdaeb96f5b914 to your computer and use it in GitHub Desktop.
caching and promis-ing in hyperapp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="https://unpkg.com/[email protected]"></script> | |
</head> | |
<body> | |
<script src="main.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { h, app } = hyperapp; | |
const Loading = () => h("div", undefined, "loading: ", h("progress")); | |
//////////////// | |
// Promisable // | |
//////////////// | |
// Takes a function `fn` that returns either | |
// - a virtual node | |
// - a component (i.e. `props => vnode`) | |
// - a promise that resolves to a component. | |
const Promisable = fn => (props, ...children) => { | |
// `key` is used to track if this component is still in the (v)dom. | |
const key = props.key; // REQUIRED | |
// `status` contains the previously resolved component or not. | |
const status = props.status || {}; // REQUIRED | |
// `set` handles updates to the status | |
const set = props.set; // REQUIRED | |
// Note: Can also use the "mixin-closure-over-emit" (anti?)pattern to | |
// make it so "key" is the only required prop. | |
// Pros: | |
// - Don't have to combine `fn`'s props with `Promisable`'s props. | |
// Cons: | |
// - Won't work with multiple "app"'s in the same page, or would need | |
// something like "NewPromisable()". | |
// If the status has already been resolved into a component then we're done. | |
if (status.component) { | |
return status.component(props, ...children); | |
} | |
// TODO this type of lifecycle wrapping probably won't work. | |
// I'm not sure what happens if there is a list of Promisable's | |
// and two rows get swapped. Do they both get removed or updated? | |
// We DONT want `fn` to be re-evalutated in that setting since | |
// both keys still appear in the vdom. | |
const wrapNode = node => { | |
const data = Object.assign({}, node.data, { | |
_key: key, | |
onremove: el => { | |
// If this node gets removed, | |
// we assume this key is no longer in the vdom. | |
set({ key }); | |
return node.data.onremove ? node.data.onremove(el) : el.parentNode.removeChild(el); | |
}, | |
onupdate: (el, oldData) => { | |
// If this node gets updated to a new key, | |
// we assume this key is no longer in the vdom. | |
if (oldData._key && oldData._key != key) { | |
set({ key: oldData._key }); | |
} | |
return node.data.onupdate ? node.data.onupdate(el, oldData) : undefined; | |
}, | |
}); | |
return Object.assign({}, node, { data }); | |
}; | |
const wrapComponent = component => { | |
return (...args) => wrapNode(component(...args)); | |
}; | |
// Else we need to create a component that we will store | |
// in the state via the `set` method. | |
// Pros: | |
// - Time-travelling will correctly show when component finish loading. | |
// - No global cache or odd closures. | |
// Cons: | |
// - Storing components/nodes in the state keeps it from being serializable. | |
// The component is a simpler wrapper around another component | |
// that injects lifecycle methods used for cache invalidation. | |
const load = wrapComponent(Loading); // TODO make configurable | |
// Evaluate the given function and determine what type it is. | |
const promise = fn(); | |
if (promise.then && typeof promise.then === "function") { | |
// `promise` is a promise | |
promise.then(newComponent => { | |
set({ key, status: { loaded: true, component: newComponent } }); | |
}); | |
set({ key, status: { loaded: false, component: load } }); | |
return load(props, ...children); // TODO these aren't "load" props | |
} | |
if (typeof promise === "function") { | |
// `promise` is a component | |
const component = wrapComponent(promise); | |
set({ key, status: { loaded: true, component } }); | |
return component(props, ...children); | |
} | |
if (promise.tag && promise.data) { | |
// `promise` is a vnode | |
const component = wrapComponent(() => promise); | |
set({ key, status: { loaded: true, component } }); | |
return component(props, ...children); | |
} | |
throw new Error(`unknown return value of "fn": ${typeof promise}`); | |
}; | |
const PromisableMixin = () => ({ | |
state: { statuses: {} }, | |
actions: { | |
setStatus: (state, actions, data) => { | |
const key = data.key; | |
const status = data.status; | |
// If a status is not given, then we delete the key from `state.statuses`. | |
if (status === undefined) { | |
const statuses = Object.assign({}, state.statuses); | |
delete statuses[key]; | |
return { statuses }; | |
} | |
const currentStatus = state.statuses[key] || {}; | |
// A status can only go to `true` if it was at a `false` state. | |
if (status.loaded === true && currentStatus.loaded !== false) { | |
return; | |
} | |
// Otherwise we set `statuses[key] = status`. | |
return { | |
statuses: Object.assign({}, state.statuses, { [data.key]: data.status }), | |
}; | |
}, | |
}, | |
}); | |
// Example: | |
const A = () => | |
new Promise(resolve => | |
setTimeout(() => resolve(props => h("div", undefined, `done: ${props.key}`)), 1000), | |
); | |
const B = () => () => h("div", undefined, "regular component"); | |
const C = () => h("div", undefined, "regular vnode"); | |
const pA = Promisable(A); | |
const pB = Promisable(B); | |
const pC = Promisable(C); | |
app({ | |
state: { | |
X: "", | |
}, | |
view: (state, actions) => | |
h("main", undefined, [ | |
pA({ | |
key: `A:${state.X}`, | |
status: state.statuses[`A:${state.X}`], | |
set: actions.setStatus, | |
}), | |
pA({ | |
key: "static A", | |
status: state.statuses["static A"], | |
set: actions.setStatus, | |
}), | |
pB({ key: "B", status: state.statuses["B"], set: actions.setStatus }), | |
pC({ key: "C", status: state.statuses["C"], set: actions.setStatus }), | |
h("h3", undefined, state.X), | |
h("input", { | |
type: "text", | |
value: state.X, | |
oninput: e => actions.setX(e.target.value), | |
}), | |
]), | |
actions: { | |
setX: (_, __, X) => ({ X }), | |
}, | |
mixins: [PromisableMixin], | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { h, app } = hyperapp; | |
const Loading = () => h("div", undefined, "loading: ", h("progress")); | |
// This is the same as `main.js` but without trying to clear cache. | |
// Also it only accepts things that return a promise of a component. | |
const Promisable = fn => (props, ...children) => { | |
// `status` contains the previously resolved component or not. | |
const status = props.status || {}; // REQUIRED | |
// `set` handles updates to the status | |
const set = props.set; // REQUIRED | |
// If the status has already been resolved into a component then we're done. | |
if (status.component) { | |
return status.component(props, ...children); | |
} | |
const load = Loading; // TODO make configurable | |
// Evaluate the given function and determine what type it is. | |
const promise = fn(); | |
if (promise.then && typeof promise.then === "function") { | |
// `promise` is a promise | |
promise.then(newComponent => { | |
set({ loaded: true, component: newComponent }); | |
}); | |
set({ loaded: false, component: load }); | |
return load(props, ...children); // TODO these aren't "load" props | |
} | |
if (typeof promise === "function") { | |
// `promise` is a component | |
const component = promise; | |
set({ loaded: true, component }); | |
return component(props, ...children); | |
} | |
throw new Error(`unknown return value of "fn": ${typeof promise}`); | |
}; | |
const PromisableMixin = () => ({ | |
state: { statuses: {} }, | |
actions: { | |
setStatus: (state, actions, data) => { | |
const key = data.key; | |
const status = data.status; | |
// If a status is not given, then we delete the key from `state.statuses`. | |
if (status === undefined) { | |
const statuses = Object.assign({}, state.statuses); | |
delete statuses[key]; | |
return { statuses }; | |
} | |
const currentStatus = state.statuses[key] || {}; | |
// A status can only go to `true` if it was at a `false` state. | |
if (status.loaded === true && currentStatus.loaded !== false) { | |
return; | |
} | |
// Otherwise we set `statuses[key] = status`. | |
return { | |
statuses: Object.assign({}, state.statuses, { [data.key]: data.status }), | |
}; | |
}, | |
}, | |
}); | |
// Example: | |
const A = () => | |
new Promise(resolve => | |
setTimeout(() => resolve(props => h("div", undefined, `done: ${props.key}`)), 1000), | |
); | |
const B = () => h("div", undefined, "regular component"); | |
const pA = Promisable(A); | |
const pB = Promisable(() => B); | |
app({ | |
state: { X: "" }, | |
view: (state, actions) => | |
h("main", undefined, [ | |
pA({ | |
key: `A:${state.X}`, | |
status: state.statuses[`A:${state.X}`], | |
set: status => actions.setStatus({ key: `A:${state.X}`, status }), | |
}), | |
pA({ | |
key: "static A", | |
status: state.statuses["static A"], | |
set: status => actions.setStatus({ key: "static A", status }), | |
}), | |
pB({ status: state.statuses["B"], set: status => actions.setStatus({ key: "B", status }) }), | |
h("h3", undefined, state.X), | |
h("input", { | |
type: "text", | |
value: state.X, | |
oninput: e => actions.setX(e.target.value), | |
}), | |
]), | |
actions: { | |
setX: (_, __, X) => ({ X }), | |
}, | |
mixins: [PromisableMixin], | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment