Files
buuh/index.js
T

253 lines
7.2 KiB
JavaScript
Raw Normal View History

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-06-04 15:03:56 -04:00
const hash = require('sheet-router/hash')
const hashMatch = require('hash-match')
2016-05-11 13:51:17 +07:00
const sendAction = require('send-action')
2016-05-29 12:31:20 +09:00
const mutate = require('xtend/mutable')
2016-05-23 10:44:32 +09:00
const assert = require('assert')
2016-05-11 13:51:17 +07:00
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-23 10:44:32 +09:00
// framework for creating sturdy web applications
2016-05-11 13:51:17 +07:00
// 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
2016-05-23 10:44:32 +09:00
// (str, obj) -> str
2016-05-23 23:39:22 +09:00
function toString (route, serverState) {
2016-05-21 21:59:03 +09:00
const initialState = {}
2016-05-23 23:39:22 +09:00
const nsState = {}
2016-05-21 21:59:03 +09:00
_models.forEach(function (model) {
2016-05-23 23:39:22 +09:00
const ns = model.namespace
if (ns) {
if (!nsState[ns]) nsState[ns] = {}
apply(ns, model.state, nsState)
nsState[ns] = xtend(nsState[ns], serverState[ns])
} else {
apply(model.namespace, model.state, initialState)
}
2016-05-21 21:59:03 +09:00
})
2016-05-23 23:39:22 +09:00
const state = xtend(initialState, xtend(serverState, nsState))
const tree = _router(route, state, function () {
2016-05-21 21:59:03 +09:00
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 23:39:22 +09:00
// (str?, obj?) -> DOMNode
function start (rootId, opts) {
if (!opts) opts = rootId
2016-05-23 02:45:15 +09:00
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-23 10:44:32 +09:00
// send() is used to trigger actions inside
// views, effects and subscriptions
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-23 10:44:32 +09:00
// subscriptions are loaded after sendAction() is called
// because they both need access to send() and can't
// react to actions (read-only)
2016-05-16 04:09:02 +07:00
_models.forEach(function (model) {
if (model.subscriptions) {
2016-05-23 10:44:32 +09:00
assert.ok(Array.isArray(model.subscriptions, 'subs must be an array'))
2016-05-16 04:09:02 +07:00
model.subscriptions.forEach(function (sub) {
sub(send)
})
}
})
2016-05-12 14:45:20 +07:00
2016-05-23 23:39:22 +09:00
// If an id is provided, the application will rehydrate
// on the node. If no id is provided it will return
// a tree that's ready to be appended to the DOM.
//
// The rootId is determined to find the application root
2016-05-23 10:44:32 +09:00
// on update. Since the DOM nodes change between updates,
// we must call document.querySelector() to find the root.
// Use different names when loading multiple choo applications
// on the same page
2016-05-23 23:39:22 +09:00
if (rootId) {
document.addEventListener('DOMContentLoaded', function (event) {
rootId = rootId.replace(/^#/, '')
const oldTree = document.querySelector('#' + rootId)
assert.ok(oldTree, 'could not find node #' + rootId)
const newTree = _router(send.state().app.location, send.state(), send)
yo.update(oldTree, newTree)
})
} else {
rootId = name + '-root'
const tree = _router(send.state().app.location, send.state(), send)
tree.setAttribute('id', rootId)
return tree
}
2016-05-11 13:51:17 +07:00
2016-05-23 10:44:32 +09:00
// handle an action by either reducers, effects
// or both - return the new state when done
// (obj, obj, fn) -> obj
2016-05-16 04:09:02 +07:00
function handleAction (action, state, send) {
2016-05-23 10:44:32 +09:00
var reducersCalled = false
var effectsCalled = false
2016-05-29 12:31:20 +09:00
const newState = xtend(state)
2016-05-16 04:09:02 +07:00
2016-05-23 10:44:32 +09:00
// validate if a namespace exists. Namespaces
// are delimited by the first ':'. Perhaps
// we'll allow recursive namespaces in the
// future - who knows
2016-05-23 02:45:15 +09:00
if (/:/.test(action.type)) {
const arr = action.type.split(':')
2016-05-23 10:44:32 +09:00
var ns = arr.shift()
action.type = arr.join(':')
2016-05-23 02:45:15 +09:00
}
2016-05-23 10:44:32 +09:00
const _reducers = ns ? reducers[ns] : reducers
if (_reducers && _reducers[action.type]) {
2016-05-23 02:45:15 +09:00
if (ns) {
2016-05-29 12:31:20 +09:00
const reducedState = _reducers[action.type](action, state[ns])
if (!newState[ns]) newState[ns] = {}
mutate(newState[ns], xtend(state[ns], reducedState))
} else {
mutate(newState, reducers[action.type](action, state))
}
2016-05-23 10:44:32 +09:00
reducersCalled = true
2016-05-16 04:09:02 +07:00
}
2016-05-23 10:44:32 +09:00
const _effects = ns ? effects[ns] : effects
if (_effects && _effects[action.type]) {
2016-05-23 14:40:52 +09:00
if (ns) _effects[action.type](action, state[ns], send)
else _effects[action.type](action, state, send)
2016-05-23 10:44:32 +09:00
effectsCalled = true
2016-05-16 04:09:02 +07:00
}
2016-05-23 10:44:32 +09:00
if (!reducersCalled && !effectsCalled) {
2016-05-16 04:09:02 +07:00
throw new Error('Could not find action ' + action.type)
}
2016-05-29 12:31:20 +09:00
// allows (newState === oldState) checks
return (reducersCalled) ? newState : state
2016-05-16 04:09:02 +07:00
}
2016-05-23 10:44:32 +09:00
// update the DOM after every state mutation
// (obj, obj) -> null
2016-05-29 12:31:20 +09:00
function onchange (action, newState, oldState) {
if (newState === oldState) return
2016-05-12 14:45:20 +07:00
const oldTree = document.querySelector('#' + rootId)
2016-05-31 15:54:46 +02:00
assert.ok(oldTree, "Could not find DOM node '#" + rootId + "' to update")
2016-05-29 12:31:20 +09:00
const newTree = _router(newState.app.location, newState, send, oldState)
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
}
}
2016-05-23 10:44:32 +09:00
// register all routes on the router
2016-05-11 13:51:17 +07:00
// [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) {
2016-06-04 18:43:38 -04:00
var initialLocation
if (opts.history !== false) {
initialLocation = document.location.href
} else {
initialLocation = hashMatch(document.location.hash)
}
2016-05-16 04:09:02 +07:00
const model = {
2016-05-23 02:45:15 +09:00
namespace: 'app',
2016-06-04 18:43:38 -04:00
state: { location: initialLocation },
2016-05-23 10:44:32 +09:00
subscriptions: [],
reducers: {
// handle href links
location: function setLocation (action, state) {
return {
location: action.location.replace(/#.*/, '')
}
}
}
2016-05-11 13:51:17 +07:00
}
2016-05-23 10:44:32 +09:00
// enable catching <href a=""></href> links
// enable HTML5 history API
2016-05-23 07:35:24 -04:00
if (opts.href !== false) pushLocationSub(href)
2016-06-04 18:43:38 -04:00
if (opts.history !== false) {
pushLocationSub(history)
// otherwise enable hash history
} else {
2016-06-04 15:03:56 -04:00
pushLocationSub(function (navigate) {
hash(function (fragment) {
navigate(hashMatch(fragment))
})
})
}
2016-05-16 04:09:02 +07:00
return model
2016-05-23 10:44:32 +09:00
// create a new subscription that modifies
2016-05-23 14:40:52 +09:00
// 'app:location' and push it to be loaded
2016-05-23 10:44:32 +09:00
// fn -> null
function pushLocationSub (cb) {
model.subscriptions.push(function (send) {
cb(function (href) {
send('app:location', { location: href })
})
})
2016-05-11 13:51:17 +07:00
}
}
2016-05-12 14:45:20 +07:00
// compose an object conditionally
2016-05-23 10:44:32 +09:00
// optionally contains a namespace
// which is used to nest properties.
2016-05-12 14:45:20 +07:00
// (str, obj, obj) -> null
2016-05-23 10:44:32 +09:00
function apply (ns, source, target) {
2016-05-12 14:45:20 +07:00
Object.keys(source).forEach(function (key) {
2016-05-23 10:44:32 +09:00
if (ns) {
if (!target[ns]) target[ns] = {}
target[ns][key] = source[key]
2016-05-23 02:45:15 +09:00
} else target[key] = source[key]
2016-05-12 14:45:20 +07:00
})
}