2016-05-12 14:45:20 +07:00
|
|
|
const history = require('sheet-router/history')
|
2016-05-11 13:51:17 +07:00
|
|
|
const sheetRouter = require('sheet-router')
|
|
|
|
|
const document = require('global/document')
|
2016-05-12 14:45:20 +07:00
|
|
|
const href = require('sheet-router/href')
|
2016-05-11 13:51:17 +07:00
|
|
|
const sendAction = require('send-action')
|
|
|
|
|
const xtend = require('xtend')
|
|
|
|
|
const yo = require('yo-yo')
|
2016-05-10 23:23:33 +07:00
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
choo.view = yo
|
2016-05-10 23:23:33 +07:00
|
|
|
module.exports = choo
|
|
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
// A framework for creating sturdy web applications
|
|
|
|
|
// null -> fn
|
2016-05-23 02:45:15 +09:00
|
|
|
function choo () {
|
2016-05-21 21:59:03 +09:00
|
|
|
const _models = []
|
2016-05-11 13:51:17 +07:00
|
|
|
var _router = null
|
|
|
|
|
|
2016-05-21 21:59:03 +09:00
|
|
|
start.toString = toString
|
2016-05-11 13:51:17 +07:00
|
|
|
start.router = router
|
|
|
|
|
start.model = model
|
|
|
|
|
start.start = start
|
|
|
|
|
|
|
|
|
|
return start
|
|
|
|
|
|
2016-05-21 21:59:03 +09:00
|
|
|
// render the application to a string
|
|
|
|
|
function toString (route, state) {
|
|
|
|
|
const initialState = {}
|
|
|
|
|
|
|
|
|
|
_models.forEach(function (model) {
|
|
|
|
|
if (model.state) apply(model.name, model.state, initialState)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const tree = _router(route, xtend(initialState, state), function () {
|
|
|
|
|
throw new Error('send() cannot be called on the server')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return tree.toString()
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
// start the application
|
2016-05-23 02:45:15 +09:00
|
|
|
// obj -> DOMNode
|
|
|
|
|
function start (opts) {
|
|
|
|
|
opts = opts || {}
|
|
|
|
|
const name = opts.name || 'choo'
|
2016-05-16 04:09:02 +07:00
|
|
|
const initialState = {}
|
|
|
|
|
const reducers = {}
|
|
|
|
|
const effects = {}
|
|
|
|
|
|
2016-05-21 21:59:03 +09:00
|
|
|
_models.push(appInit(opts))
|
2016-05-16 04:09:02 +07:00
|
|
|
_models.forEach(function (model) {
|
2016-05-23 02:45:15 +09:00
|
|
|
if (model.state) apply(model.namespace, model.state, initialState)
|
|
|
|
|
if (model.reducers) apply(model.namespace, model.reducers, reducers)
|
|
|
|
|
if (model.effects) apply(model.namespace, model.effects, effects)
|
2016-05-11 13:51:17 +07:00
|
|
|
})
|
|
|
|
|
|
2016-05-16 04:09:02 +07:00
|
|
|
const send = sendAction({
|
|
|
|
|
onaction: handleAction,
|
|
|
|
|
onchange: onchange,
|
|
|
|
|
state: initialState
|
|
|
|
|
})
|
2016-05-12 14:45:20 +07:00
|
|
|
|
2016-05-16 04:09:02 +07:00
|
|
|
_models.forEach(function (model) {
|
|
|
|
|
if (model.subscriptions) {
|
|
|
|
|
model.subscriptions.forEach(function (sub) {
|
|
|
|
|
sub(send)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
2016-05-12 14:45:20 +07:00
|
|
|
|
|
|
|
|
const rootId = name + '-root'
|
2016-05-23 02:45:15 +09:00
|
|
|
const tree = _router(send.state().app.location, send.state(), send)
|
2016-05-12 14:45:20 +07:00
|
|
|
tree.setAttribute('id', rootId)
|
2016-05-11 14:56:18 +07:00
|
|
|
return tree
|
2016-05-11 13:51:17 +07:00
|
|
|
|
2016-05-16 04:09:02 +07:00
|
|
|
function handleAction (action, state, send) {
|
|
|
|
|
var _reducers = false
|
|
|
|
|
var _effects = false
|
|
|
|
|
var newState = null
|
|
|
|
|
|
2016-05-23 02:45:15 +09:00
|
|
|
if (/:/.test(action.type)) {
|
|
|
|
|
const arr = action.type.split(':')
|
|
|
|
|
var ns = arr[0]
|
|
|
|
|
action.type = arr[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nsReducers = ns ? reducers[ns] : reducers
|
2016-05-23 04:00:29 +09:00
|
|
|
if (nsReducers && nsReducers[action.type]) {
|
2016-05-23 02:45:15 +09:00
|
|
|
if (ns) {
|
|
|
|
|
state[ns] = reducers[ns][action.type](action, state[ns])
|
|
|
|
|
newState = state
|
|
|
|
|
} else {
|
|
|
|
|
newState = xtend(state, reducers[action.type](action, state))
|
|
|
|
|
}
|
2016-05-16 04:09:02 +07:00
|
|
|
_reducers = true
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-23 02:45:15 +09:00
|
|
|
const nsEffects = ns ? effects[ns] : effects
|
2016-05-23 04:00:29 +09:00
|
|
|
if (nsEffects && nsEffects[action.type]) {
|
2016-05-23 02:45:15 +09:00
|
|
|
nsEffects[action.type](action, newState || state)
|
2016-05-16 04:09:02 +07:00
|
|
|
_effects = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_reducers && !_effects) {
|
|
|
|
|
throw new Error('Could not find action ' + action.type)
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-23 02:45:15 +09:00
|
|
|
return newState || state
|
2016-05-16 04:09:02 +07:00
|
|
|
}
|
|
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
// update on every change
|
|
|
|
|
function onchange (action, state) {
|
2016-05-12 14:45:20 +07:00
|
|
|
const oldTree = document.querySelector('#' + rootId)
|
2016-05-23 02:45:15 +09:00
|
|
|
const newTree = _router(state.app.location, state, send)
|
2016-05-12 14:45:20 +07:00
|
|
|
newTree.setAttribute('id', rootId)
|
2016-05-11 14:56:18 +07:00
|
|
|
yo.update(oldTree, newTree)
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// register all routes
|
|
|
|
|
// [obj|fn] -> null
|
|
|
|
|
function router (cb) {
|
|
|
|
|
_router = sheetRouter(cb)
|
2016-05-16 03:37:19 +07:00
|
|
|
return _router
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create a new model
|
|
|
|
|
// (str?, obj) -> null
|
2016-05-23 02:45:15 +09:00
|
|
|
function model (model) {
|
2016-05-11 13:51:17 +07:00
|
|
|
_models.push(model)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-12 14:45:20 +07:00
|
|
|
// initial application state model
|
2016-05-16 04:09:02 +07:00
|
|
|
// obj -> obj
|
|
|
|
|
function appInit (opts) {
|
|
|
|
|
const model = {
|
2016-05-23 02:45:15 +09:00
|
|
|
namespace: 'app',
|
2016-05-22 20:54:39 +09:00
|
|
|
state: { location: document.location.href },
|
|
|
|
|
reducers: { location: setLocation },
|
2016-05-16 04:09:02 +07:00
|
|
|
subscriptions: []
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
|
2016-05-16 04:09:02 +07:00
|
|
|
if (opts.href !== false) {
|
|
|
|
|
model.subscriptions.push(function (send) {
|
|
|
|
|
href(function (href) {
|
2016-05-23 04:00:29 +09:00
|
|
|
send('app:location', { location: href })
|
2016-05-16 04:09:02 +07:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opts.history !== false) {
|
|
|
|
|
model.subscriptions.push(function (send) {
|
|
|
|
|
history(function (href) {
|
2016-05-23 04:00:29 +09:00
|
|
|
send('app:location', { location: href })
|
2016-05-16 04:09:02 +07:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return model
|
|
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
// handle href links
|
|
|
|
|
function setLocation (action, state) {
|
2016-05-23 04:00:29 +09:00
|
|
|
return { location: action.location.replace(/#.*/, '') }
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-12 14:45:20 +07:00
|
|
|
// compose an object conditionally
|
|
|
|
|
// (str, obj, obj) -> null
|
|
|
|
|
function apply (name, source, target) {
|
|
|
|
|
Object.keys(source).forEach(function (key) {
|
2016-05-23 02:45:15 +09:00
|
|
|
if (name) {
|
|
|
|
|
if (!target[name]) target[name] = {}
|
|
|
|
|
target[name][key] = source[key]
|
|
|
|
|
target[name][key].namespace = name
|
|
|
|
|
} else target[key] = source[key]
|
2016-05-12 14:45:20 +07:00
|
|
|
})
|
|
|
|
|
}
|