Skip to content

Instantly share code, notes, and snippets.

@torgeir
Last active July 19, 2019 06:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save torgeir/6d1810d008d5764fcf37d9fc21093904 to your computer and use it in GitHub Desktop.
Save torgeir/6d1810d008d5764fcf37d9fc21093904 to your computer and use it in GitHub Desktop.
A plain js take on @ccorcos elmish
const declare = function (dispatch, state) {
const view = function () {
if (state.error) {
return <p>Error: { state.error }</p>;
}
if (state.loading) {
return <p>Clock: Loading..</p>;
}
return <p>Clock: { `${state.date} ${state.time}` }</p>;
}
// first request wins
const http = state.loading
? [{
key: 'lookup-time',
url: 'http://time.jsontest.com/',
success: (json) => dispatch('lookup-time-done', { json }),
error: (error) => dispatch('lookup-time-error', { error })
}, {
key: 'lookup-date',
url: 'http://date.jsontest.com/',
success: (json) => dispatch('lookup-date-done', { json }),
error: (error) => dispatch('lookup-date-error', { error })
}]
: [];
return { view, http };
};
const initialState = { loading: true };
const update = function (state = initialState, action) {
switch (action.type) {
case 'lookup-time-error':
return { loading: false, error: `lookup-time-error: ${action.error.message || 'an error occurred'}` };
case 'lookup-date-error':
return { loading: false, error: `lookup-date-error: ${action.error.message || 'an error occurred'}` };
case 'lookup-time-done':
return { loading: false, time: action.json.time, date: action.json.date };
case 'lookup-date-done':
return { loading: false, time: action.json.time, date: action.json.date };
}
return state;
};
const App = { declare, update };
run(debuggable(App), [
render(),
fetch(),
]);
/* inspired by https://github.com/ccorcos/elmish/ */
function run (Component, services = []) {
let isThrottled = false;
const throttled = function (fn) {
isThrottled = true;
fn();
isThrottled = false;
};
let state;
const dispatch = function (type, data) {
state = window.state = Component.update(state, { type, ...data });
if (!isThrottled) {
const effects = Component.declare(dispatch, state);
services.forEach(service => service(effects, throttled));
}
};
dispatch(undefined);
return { state, dispatch };
}
// composable components
function debuggable (Component) {
const update = function (state, action) {
return Component.update(state, action);
};
const declare = function (dispatch, state) {
const loggingDispatch = function (type, data) {
console.log('dispatching', type, data);
return dispatch(type, data);
};
return Component.declare(loggingDispatch, state);
};
return { update, declare };
}
// services, for side-effects
function render () {
return ({ view }) =>
ReactDom.render(typeof view == 'function' ? view() : view, el);
}
function fetch () {
let pending = [];
return function ({ http = [] }) {
const staleHttpRequests = Object.keys(pending)
.map(key => pending[key])
.filter((req) => http.filter(r => r.key == req.key).length == 0);
staleHttpRequests.forEach((req) =>
console.log(`request ${req.key} is no longer pending and could be aborted`));
const newRequests = http.filter((req) => !(req.key in pending));
pending = http.reduce(function (acc, req) {
acc[req.key] = req;
return acc;
}, {});
newRequests.forEach(function (req) {
const { key, url } = req;
const stillPending = () => key in pending;
window.fetch(url)
.then(res => res.json())
.then(function (json) {
if (!stillPending()) {
return;
}
delete pending[key];
req.success(json);
})
.catch(function (err) {
if (!stillPending()) {
return;
}
delete pending[key];
req.error(err);
});
});
};
};
# link to runnable example, copy it into a browser
http://omniscientjs.github.io/playground/#const%20declare%20%3D%20function%20(dispatch%2C%20state)%20%7B%0A%20%20%0A%20%20const%20view%20%3D%20function%20()%20%7B%0A%20%20%20%20if%20(state.error)%20%7B%0A%20%20%20%20%20%20return%20%3Cp%3EError%3A%20%7B%20state.error%20%7D%3C%2Fp%3E%3B%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20if%20(state.loading)%20%7B%0A%20%20%20%20%20%20return%20%3Cp%3EClock%3A%20Loading..%3C%2Fp%3E%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20%3Cp%3EClock%3A%20%7B%20%60%24%7Bstate.date%7D%20%24%7Bstate.time%7D%60%20%7D%3C%2Fp%3E%3B%0A%20%20%7D%0A%20%20%0A%20%20%2F%2F%20first%20request%20wins%0A%20%20const%20http%20%3D%20state.loading%0A%20%20%20%20%3F%20%5B%7B%20%0A%20%20%20%20%20%20%20%20key%3A%20'lookup-time'%2C%20%0A%20%20%20%20%20%20%20%20url%3A%20'http%3A%2F%2Ftime.jsontest.com%2F'%2C%0A%20%20%20%20%20%20%20%20success%3A%20(json)%20%3D%3E%20dispatch('lookup-time-done'%2C%20%7B%20json%20%7D)%2C%0A%20%20%20%20%20%20%20%20error%3A%20(error)%20%3D%3E%20dispatch('lookup-time-error'%2C%20%7B%20error%20%7D)%0A%20%20%20%20%20%20%7D%2C%20%7B%20%0A%20%20%20%20%20%20%20%20key%3A%20'lookup-date'%2C%20%0A%20%20%20%20%20%20%20%20url%3A%20'http%3A%2F%2Fdate.jsontest.com%2F'%2C%0A%20%20%20%20%20%20%20%20success%3A%20(json)%20%3D%3E%20dispatch('lookup-date-done'%2C%20%7B%20json%20%7D)%2C%0A%20%20%20%20%20%20%20%20error%3A%20(error)%20%3D%3E%20dispatch('lookup-date-error'%2C%20%7B%20error%20%7D)%0A%20%20%20%20%20%20%7D%5D%0A%20%20%20%20%3A%20%5B%5D%3B%0A%20%20%0A%20%20return%20%7B%20view%2C%20http%20%7D%3B%0A%7D%3B%0A%0A%0Aconst%20initialState%20%3D%20%7B%20loading%3A%20true%20%7D%3B%0Aconst%20update%20%3D%20function%20(state%20%3D%20initialState%2C%20action)%20%7B%0A%20%20%0A%20%20switch%20(action.type)%20%7B%0A%20%20%20%20case%20'lookup-time-error'%3A%20%0A%20%20%20%20%20%20return%20%7B%20loading%3A%20false%2C%20error%3A%20%60lookup-time-error%3A%20%24%7Baction.error.message%20%7C%7C%20'an%20error%20occurred'%7D%60%20%7D%3B%0A%0A%20%20%20%20case%20'lookup-date-error'%3A%20%0A%20%20%20%20%20%20return%20%7B%20loading%3A%20false%2C%20error%3A%20%60lookup-date-error%3A%20%24%7Baction.error.message%20%7C%7C%20'an%20error%20occurred'%7D%60%20%7D%3B%0A%20%20%0A%20%20%20%20case%20'lookup-time-done'%3A%20%0A%20%20%20%20%20%20return%20%7B%20loading%3A%20false%2C%20time%3A%20action.json.time%2C%20date%3A%20action.json.date%20%7D%3B%0A%0A%20%20%20%20case%20'lookup-date-done'%3A%20%0A%20%20%20%20%20%20return%20%7B%20loading%3A%20false%2C%20time%3A%20action.json.time%2C%20date%3A%20action.json.date%20%7D%3B%0A%20%20%7D%0A%20%20%0A%20%20return%20state%3B%0A%7D%3B%0A%0A%0Aconst%20App%20%3D%20%7B%20declare%2C%20update%20%7D%3B%0A%0A%0Arun(debuggable(App)%2C%20%5B%0A%20%20render()%2C%0A%20%20fetch()%2C%0A%5D)%3B%0A%0A%0A%2F*%20inspired%20by%20https%3A%2F%2Fgithub.com%2Fccorcos%2Felmish%2F%20*%2F%0Afunction%20run%20(Component%2C%20services%20%3D%20%5B%5D)%20%7B%0A%0A%20%20let%20isThrottled%20%3D%20false%3B%0A%20%20const%20throttled%20%3D%20function%20(fn)%20%7B%0A%20%20%20%20isThrottled%20%3D%20true%3B%0A%20%20%20%20fn()%3B%0A%20%20%20%20isThrottled%20%3D%20false%3B%0A%20%20%7D%3B%0A%0A%20%20let%20state%3B%0A%0A%20%20const%20dispatch%20%3D%20function%20(type%2C%20data)%20%7B%0A%20%20%20%20state%20%3D%20window.state%20%3D%20Component.update(state%2C%20%7B%20type%2C%20...data%20%7D)%3B%0A%20%20%20%20if%20(!isThrottled)%20%7B%0A%20%20%20%20%20%20const%20effects%20%3D%20Component.declare(dispatch%2C%20state)%3B%0A%20%20%20%20%20%20services.forEach(service%20%3D%3E%20service(effects%2C%20throttled))%3B%0A%20%20%20%20%7D%0A%20%20%7D%3B%0A%0A%20%20dispatch(undefined)%3B%0A%0A%20%20return%20%7B%20state%2C%20dispatch%20%7D%3B%0A%7D%0A%0A%0A%2F%2F%20composable%20components%0A%0Afunction%20debuggable%20(Component)%20%7B%0A%0A%20%20const%20update%20%3D%20function%20(state%2C%20action)%20%7B%0A%20%20%20%20return%20Component.update(state%2C%20action)%3B%0A%20%20%7D%3B%0A%0A%20%20const%20declare%20%3D%20function%20(dispatch%2C%20state)%20%7B%0A%20%20%20%20const%20loggingDispatch%20%3D%20function%20(type%2C%20data)%20%7B%0A%20%20%20%20%20%20console.log('dispatching'%2C%20type%2C%20data)%3B%0A%20%20%20%20%20%20return%20dispatch(type%2C%20data)%3B%0A%20%20%20%20%7D%3B%0A%20%20%20%20return%20Component.declare(loggingDispatch%2C%20state)%3B%0A%20%20%7D%3B%0A%0A%20%20return%20%7B%20update%2C%20declare%20%7D%3B%0A%7D%0A%0A%0A%2F%2F%20services%2C%20for%20side-effects%0A%0Afunction%20render%20()%20%7B%0A%20%20return%20(%7B%20view%20%7D)%20%3D%3E%0A%20%20%20%20ReactDom.render(typeof%20view%20%3D%3D%20'function'%20%3F%20view()%20%3A%20view%2C%20el)%3B%0A%7D%0A%0Afunction%20fetch%20()%20%7B%0A%0A%20%20let%20pending%20%3D%20%5B%5D%3B%0A%0A%20%20return%20function%20(%7B%20http%20%3D%20%5B%5D%20%7D)%20%7B%0A%0A%20%20%20%20const%20staleHttpRequests%20%3D%20Object.keys(pending)%0A%20%20%20%20%20%20.map(key%20%3D%3E%20pending%5Bkey%5D)%0A%20%20%20%20%20%20.filter((req)%20%3D%3E%20http.filter(r%20%3D%3E%20r.key%20%3D%3D%20req.key).length%20%3D%3D%200)%3B%0A%0A%20%20%20%20staleHttpRequests.forEach((req)%20%3D%3E%0A%20%20%20%20%20%20console.log(%60request%20%24%7Breq.key%7D%20is%20no%20longer%20pending%20and%20could%20be%20aborted%60))%3B%0A%0A%20%20%20%20const%20newRequests%20%3D%20http.filter((req)%20%3D%3E%20!(req.key%20in%20pending))%3B%0A%0A%20%20%20%20pending%20%3D%20http.reduce(function%20(acc%2C%20req)%20%7B%0A%20%20%20%20%20%20acc%5Breq.key%5D%20%3D%20req%3B%0A%20%20%20%20%20%20return%20acc%3B%0A%20%20%20%20%7D%2C%20%7B%7D)%3B%0A%0A%20%20%20%20newRequests.forEach(function%20(req)%20%7B%0A%20%20%20%20%20%20const%20%7B%20key%2C%20url%20%7D%20%3D%20req%3B%0A%0A%20%20%20%20%20%20const%20stillPending%20%3D%20()%20%3D%3E%20key%20in%20pending%3B%0A%0A%20%20%20%20%20%20window.fetch(url)%0A%20%20%20%20%20%20%20%20.then(res%20%3D%3E%20res.json())%0A%20%20%20%20%20%20%20%20.then(function%20(json)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!stillPending())%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20delete%20pending%5Bkey%5D%3B%0A%20%20%20%20%20%20%20%20%20%20req.success(json)%3B%0A%20%20%20%20%20%20%20%20%7D)%0A%20%20%20%20%20%20%20%20.catch(function%20(err)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!stillPending())%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20delete%20pending%5Bkey%5D%3B%0A%20%20%20%20%20%20%20%20%20%20req.error(err)%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D)%3B%0A%20%20%7D%3B%0A%7D%3B%0A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment