Skip to content

Instantly share code, notes, and snippets.

@tscholl2
Last active August 31, 2017 02:39
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 tscholl2/a264e43eb3a4185c2ebcdaeb96f5b914 to your computer and use it in GitHub Desktop.
Save tscholl2/a264e43eb3a4185c2ebcdaeb96f5b914 to your computer and use it in GitHub Desktop.
caching and promis-ing in hyperapp
<html>
<head>
<script src="https://unpkg.com/hyperapp@0.12.0"></script>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
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],
});
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