Skip to content

Instantly share code, notes, and snippets.

@mnichols
Created February 9, 2017 17:18
Show Gist options
  • Save mnichols/c667bca1a6dfa729e72c2b5637af7db7 to your computer and use it in GitHub Desktop.
Save mnichols/c667bca1a6dfa729e72c2b5637af7db7 to your computer and use it in GitHub Desktop.
const noop = () => {}
const identity = obj => obj
function bus() {
let subscription = () => {}
return {
publish(...data) {
return subscription(...data)
},
subscribe(fn) {
return subscription = fn
}
}
}
export default function pal(id) {
let allActions = []
let allReceives = []
let allStates = []
let allInitialModels = []
let allReadies = []
let allNexts = []
let allPostRenders = []
let componentBus = bus()
function createComponent(cfg) {
if(cfg.initialModel) {
if(typeof cfg.initialModel !== 'function') {
throw new Error('component must implement `initialModel` as a function which receives the root model')
}
allInitialModels.push(cfg.initialModel)
}
let propose = componentBus.publish
let actions = (cfg.actions ? cfg.actions({ propose }) : propose)
if(cfg.actions) {
allActions.push(cfg.actions)
}
if(cfg.receive) {
allReceives.push(cfg.receive)
}
if(cfg.state) {
allStates.push(cfg.state)
}
if(cfg.ready) {
allReadies.push(() => cfg.ready({ cfg, actions, propose }))
}
if(cfg.nextAction) {
allNexts.push( ({model, proposal}) => cfg.nextAction({ model, proposal, propose, actions}))
}
if(cfg.postRender) {
allPostRenders.push(cfg.postRender)
}
function render(state) {
return (cfg.view ? cfg.view(state, actions) : noop)
}
render.displayName = (cfg.displayName || cfg.view.displayName)
return render
}
function nullRenderer(state, component) {
return component(state)
}
const copy = (obj) => JSON.parse(JSON.stringify(obj))
function start(cfg) {
let truth = (cfg.initialModel || {})
let renderer = (cfg.renderer || nullRenderer)
let component = (cfg.rootComponent || cfg.component || identity)
//accumulate models
for(let i = 0, len = allInitialModels.length; i < len; i++) {
allInitialModels[i]({ model: truth })
}
let computeState = (cfg.state || (({ model }) => model))
for(let i = 0, len = allStates.length; i < len; i++) {
const prevState = computeState
computeState = ({ model }) => {
let state = allStates[i]({ model, state: prevState({ model }) })
if(state){
return state
}
throw new Error('Returning a falsy value from a state function is probably a bug.')
}
}
const handleRender = (model) => {
if(!model) {
throw new Error('Model cannot be empty.')
}
let state = computeState({ model })
let result = renderer(state, component);
for(let i = 0, len = allPostRenders.length; i < len; i++) {
allPostRenders[i]({ model, state })
}
return result
}
const handleProposal = (proposal) => {
for(let i = 0; i < allReceives.length; i++) {
//confirm we didnt receive undefined
truth = allReceives[i]({ model: truth, proposal})
if(!truth) {
throw new Error(`${ typeof truth } is not a valid model. Be sure you are returning the model after a 'receive' call.`)
}
}
handleRender(truth)
for(let i = 0, len = allNexts.length; i < len; i++) {
allNexts[i]({ model: truth, proposal })
}
return true
}
//subscriptions
componentBus.subscribe(handleProposal)
//start up tasks
handleRender(truth)
for(let i = 0, len = allReadies.length; i < len; i++) {
allReadies[i]()
}
return handleRender.bind(null, truth )
}
return {
start,
createComponent,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment