* Barracks must get an initial state or state.location is undefined * Replaces document.addEventListener w/ pkg handling existing ready doc
172 lines
5.0 KiB
JavaScript
172 lines
5.0 KiB
JavaScript
const history = require('sheet-router/history')
|
|
const sheetRouter = require('sheet-router')
|
|
const document = require('global/document')
|
|
const onReady = require('document-ready')
|
|
const href = require('sheet-router/href')
|
|
const hash = require('sheet-router/hash')
|
|
const hashMatch = require('hash-match')
|
|
const barracks = require('barracks')
|
|
const assert = require('assert')
|
|
const xtend = require('xtend')
|
|
const yo = require('yo-yo')
|
|
|
|
module.exports = choo
|
|
|
|
// framework for creating sturdy web applications
|
|
// null -> fn
|
|
function choo (opts) {
|
|
opts = opts || {}
|
|
|
|
const _store = start._store = barracks(xtend(opts, { onState: render }))
|
|
var _router = start._router = null
|
|
var _defaultRoute = null
|
|
var _rootNode = null
|
|
var _routes = null
|
|
|
|
start.toString = toString
|
|
start.router = router
|
|
start.model = model
|
|
start.start = start
|
|
|
|
return start
|
|
|
|
// render the application to a string
|
|
// (str, obj) -> str
|
|
function toString (route, serverState) {
|
|
serverState = serverState || {}
|
|
assert.equal(typeof route, 'string', 'choo.app.toString: route must be a string')
|
|
assert.equal(typeof serverState, 'object', 'choo.app.toString: serverState must be an object')
|
|
_store.start({ noSubscriptions: true, noReducers: true, noEffects: true })
|
|
|
|
const state = _store.state({ state: serverState })
|
|
const router = createRouter(_defaultRoute, _routes, createSend)
|
|
const tree = router(route, state)
|
|
return tree.outerHTML || tree.toString()
|
|
|
|
function createSend () {
|
|
return function send () {
|
|
assert.fail('choo: send() cannot be called from Node')
|
|
}
|
|
}
|
|
}
|
|
|
|
// start the application
|
|
// (str?, obj?) -> DOMNode
|
|
function start (selector, startOpts) {
|
|
if (!startOpts && typeof selector !== 'string') {
|
|
startOpts = selector
|
|
selector = null
|
|
}
|
|
startOpts = startOpts || {}
|
|
|
|
_store.model(appInit(startOpts))
|
|
const createSend = _store.start(startOpts)
|
|
_router = start._router = createRouter(_defaultRoute, _routes, createSend)
|
|
const state = _store.state({state: {}})
|
|
|
|
if (!selector) {
|
|
const tree = _router(state.location.pathname, state)
|
|
_rootNode = tree
|
|
return tree
|
|
} else {
|
|
onReady(function onReady () {
|
|
const oldTree = document.querySelector(selector)
|
|
assert.ok(oldTree, 'could not query selector: ' + selector)
|
|
const newTree = _router(state.location.pathname, state)
|
|
_rootNode = yo.update(oldTree, newTree)
|
|
})
|
|
}
|
|
}
|
|
|
|
// update the DOM after every state mutation
|
|
// (obj, obj, obj, str, fn) -> null
|
|
function render (data, state, prev, name, createSend) {
|
|
if (opts.onState) opts.onState(data, state, prev, name, createSend)
|
|
if (state === prev) return
|
|
|
|
const newTree = _router(state.location.pathname, state, prev)
|
|
_rootNode = yo.update(_rootNode, newTree)
|
|
}
|
|
|
|
// register all routes on the router
|
|
// (str?, [fn|[fn]]) -> obj
|
|
function router (defaultRoute, routes) {
|
|
_defaultRoute = defaultRoute
|
|
_routes = routes
|
|
}
|
|
|
|
// create a new model
|
|
// (str?, obj) -> null
|
|
function model (model) {
|
|
_store.model(model)
|
|
}
|
|
|
|
// create a new router with a custom `createRoute()` function
|
|
// (str?, obj, fn?) -> null
|
|
function createRouter (defaultRoute, routes, createSend) {
|
|
var prev = {}
|
|
return sheetRouter(defaultRoute, routes, createRoute)
|
|
|
|
function createRoute (routeFn) {
|
|
return function (route, inline, child) {
|
|
if (typeof inline === 'function') {
|
|
inline = wrap(inline, route)
|
|
}
|
|
return routeFn(route, inline, child)
|
|
}
|
|
|
|
function wrap (child, route) {
|
|
const send = createSend(route, true)
|
|
return function chooWrap (params, state) {
|
|
const nwPrev = prev
|
|
const nwState = prev = xtend(state, { params: params })
|
|
if (!opts.noFreeze) Object.freeze(nwState)
|
|
return child(nwState, nwPrev, send)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// initial application state model
|
|
// obj -> obj
|
|
function appInit (opts) {
|
|
const loc = document.location
|
|
const state = { pathname: (opts.hash) ? hashMatch(loc.hash) : loc.href }
|
|
const reducers = {
|
|
setLocation: function setLocation (data, state) {
|
|
return { pathname: data.location.replace(/#.*/, '') }
|
|
}
|
|
}
|
|
// if hash routing explicitly enabled, subscribe to it
|
|
const subs = {}
|
|
if (opts.hash === true) {
|
|
pushLocationSub(function (navigate) {
|
|
hash(function (fragment) {
|
|
navigate(hashMatch(fragment))
|
|
})
|
|
}, 'handleHash', subs)
|
|
} else {
|
|
if (opts.history !== false) pushLocationSub(history, 'handleHistory', subs)
|
|
if (opts.href !== false) pushLocationSub(href, 'handleHref', subs)
|
|
}
|
|
|
|
return {
|
|
namespace: 'location',
|
|
subscriptions: subs,
|
|
reducers: reducers,
|
|
state: state
|
|
}
|
|
|
|
// create a new subscription that modifies
|
|
// 'app:location' and push it to be loaded
|
|
// (fn, obj) -> null
|
|
function pushLocationSub (cb, key, model) {
|
|
model[key] = function (send, done) {
|
|
cb(function navigate (pathname) {
|
|
send('location:setLocation', { location: pathname }, done)
|
|
})
|
|
}
|
|
}
|
|
}
|