2016-12-11 19:35:29 +01:00
|
|
|
const createLocation = require('sheet-router/create-location')
|
|
|
|
|
const onHistoryChange = require('sheet-router/history')
|
2016-05-11 13:51:17 +07:00
|
|
|
const sheetRouter = require('sheet-router')
|
2016-12-11 19:35:29 +01:00
|
|
|
const onHref = require('sheet-router/href')
|
|
|
|
|
const walk = require('sheet-router/walk')
|
|
|
|
|
const mutate = require('xtend/mutable')
|
2016-06-27 01:04:21 +02:00
|
|
|
const barracks = require('barracks')
|
2016-07-05 12:14:29 +02:00
|
|
|
const nanoraf = require('nanoraf')
|
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
|
|
|
|
|
|
|
|
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-06-27 01:04:21 +02:00
|
|
|
function choo (opts) {
|
|
|
|
|
opts = opts || {}
|
|
|
|
|
|
2016-07-21 21:59:01 +02:00
|
|
|
const _store = start._store = barracks()
|
2016-07-02 00:13:13 +02:00
|
|
|
var _router = start._router = null
|
2016-12-11 19:35:29 +01:00
|
|
|
var _routerOpts = null
|
2016-06-27 01:04:21 +02:00
|
|
|
var _rootNode = null
|
2016-07-02 00:13:13 +02:00
|
|
|
var _routes = null
|
2016-07-05 12:14:29 +02:00
|
|
|
var _frame = null
|
2016-05-11 13:51:17 +07:00
|
|
|
|
2016-07-21 21:59:01 +02:00
|
|
|
_store.use({ onStateChange: render })
|
|
|
|
|
_store.use(opts)
|
|
|
|
|
|
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
|
2016-07-21 21:59:01 +02:00
|
|
|
start.use = use
|
2016-05-11 13:51:17 +07:00
|
|
|
|
|
|
|
|
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-06-27 01:04:21 +02:00
|
|
|
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')
|
2016-07-05 13:22:22 +02:00
|
|
|
_store.start({ subscriptions: false, reducers: false, effects: false })
|
2016-07-02 00:13:13 +02:00
|
|
|
|
2016-06-27 01:04:21 +02:00
|
|
|
const state = _store.state({ state: serverState })
|
2016-12-11 19:35:29 +01:00
|
|
|
const router = createRouter(_routerOpts, _routes, createSend)
|
2016-07-02 00:13:13 +02:00
|
|
|
const tree = router(route, state)
|
2016-06-30 16:42:29 -07:00
|
|
|
return tree.outerHTML || tree.toString()
|
2016-07-02 00:13:13 +02:00
|
|
|
|
|
|
|
|
function createSend () {
|
|
|
|
|
return function send () {
|
2016-07-14 15:10:08 -04:00
|
|
|
assert.ok(false, 'choo: send() cannot be called from Node')
|
2016-07-02 00:13:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
2016-05-21 21:59:03 +09:00
|
|
|
}
|
|
|
|
|
|
2016-05-11 13:51:17 +07:00
|
|
|
// start the application
|
2016-05-23 23:39:22 +09:00
|
|
|
// (str?, obj?) -> DOMNode
|
2016-12-11 19:35:29 +01:00
|
|
|
function start () {
|
|
|
|
|
_store.model(createLocationModel(opts))
|
|
|
|
|
const createSend = _store.start(opts)
|
|
|
|
|
_router = start._router = createRouter(_routerOpts, _routes, createSend)
|
2016-07-03 15:08:48 -07:00
|
|
|
const state = _store.state({state: {}})
|
2016-05-11 13:51:17 +07:00
|
|
|
|
2016-12-11 19:35:29 +01:00
|
|
|
const tree = _router(state.location.href, state)
|
2016-12-22 06:55:36 -05:00
|
|
|
assert.ok(tree, 'choo.start: the router should always return a valid DOM node')
|
|
|
|
|
assert.equal(typeof tree, 'object', 'choo.start: the router should always return a valid DOM node')
|
2016-12-11 19:35:29 +01:00
|
|
|
_rootNode = tree
|
|
|
|
|
tree.done = done
|
|
|
|
|
|
|
|
|
|
return tree
|
|
|
|
|
|
|
|
|
|
// allow a 'mount' function to return the new node
|
|
|
|
|
// html -> null
|
|
|
|
|
function done (newNode) {
|
|
|
|
|
_rootNode = newNode
|
2016-05-23 23:39:22 +09:00
|
|
|
}
|
2016-06-27 01:04:21 +02:00
|
|
|
}
|
2016-05-11 13:51:17 +07:00
|
|
|
|
2016-06-27 01:04:21 +02:00
|
|
|
// update the DOM after every state mutation
|
|
|
|
|
// (obj, obj, obj, str, fn) -> null
|
2016-12-11 19:35:29 +01:00
|
|
|
function render (state, data, prev, name, createSend) {
|
2016-07-05 12:14:29 +02:00
|
|
|
if (!_frame) {
|
|
|
|
|
_frame = nanoraf(function (state, prev) {
|
2016-12-11 19:35:29 +01:00
|
|
|
const newTree = _router(state.location.href, state, prev)
|
2016-07-05 12:14:29 +02:00
|
|
|
_rootNode = yo.update(_rootNode, newTree)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
_frame(state, prev)
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
|
2016-05-23 10:44:32 +09:00
|
|
|
// register all routes on the router
|
2016-06-23 16:44:45 +02:00
|
|
|
// (str?, [fn|[fn]]) -> obj
|
2016-07-02 00:13:13 +02:00
|
|
|
function router (defaultRoute, routes) {
|
2016-12-11 19:35:29 +01:00
|
|
|
_routerOpts = defaultRoute
|
2016-07-02 00:13:13 +02:00
|
|
|
_routes = routes
|
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-06-27 01:04:21 +02:00
|
|
|
_store.model(model)
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
2016-07-02 00:13:13 +02:00
|
|
|
|
2016-07-21 21:59:01 +02:00
|
|
|
// register a plugin
|
|
|
|
|
// (obj) -> null
|
|
|
|
|
function use (hooks) {
|
|
|
|
|
assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object')
|
|
|
|
|
_store.use(hooks)
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-02 00:13:13 +02:00
|
|
|
// create a new router with a custom `createRoute()` function
|
2016-12-11 19:35:29 +01:00
|
|
|
// (str?, obj) -> null
|
|
|
|
|
function createRouter (routerOpts, routes, createSend) {
|
|
|
|
|
var prev = null
|
|
|
|
|
if (!routes) {
|
|
|
|
|
routes = routerOpts
|
|
|
|
|
routerOpts = {}
|
|
|
|
|
}
|
|
|
|
|
routerOpts = mutate({ thunk: 'match' }, routerOpts)
|
|
|
|
|
const router = sheetRouter(routerOpts, routes)
|
|
|
|
|
walk(router, wrap)
|
2016-07-02 00:13:13 +02:00
|
|
|
|
2016-12-11 19:35:29 +01:00
|
|
|
return router
|
2016-07-02 00:13:13 +02:00
|
|
|
|
2016-12-11 19:35:29 +01:00
|
|
|
function wrap (route, handler) {
|
|
|
|
|
const send = createSend('view: ' + route, true)
|
|
|
|
|
return function chooWrap (params) {
|
|
|
|
|
return function (state) {
|
|
|
|
|
// TODO(yw): find a way to wrap handlers so params shows up in state
|
|
|
|
|
const nwState = xtend(state)
|
|
|
|
|
nwState.location = xtend(nwState.location, { params: params })
|
|
|
|
|
|
2016-12-11 15:45:46 -05:00
|
|
|
const nwPrev = prev
|
|
|
|
|
prev = nwState // save for next time
|
|
|
|
|
|
2016-07-05 13:22:22 +02:00
|
|
|
if (opts.freeze !== false) Object.freeze(nwState)
|
2016-12-11 19:35:29 +01:00
|
|
|
return handler(nwState, nwPrev, send)
|
2016-07-02 00:13:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
|
2016-12-11 19:35:29 +01:00
|
|
|
// application location model
|
2016-05-16 04:09:02 +07:00
|
|
|
// obj -> obj
|
2016-12-11 19:35:29 +01:00
|
|
|
function createLocationModel (opts) {
|
2016-06-27 01:04:21 +02:00
|
|
|
return {
|
2016-07-02 00:13:13 +02:00
|
|
|
namespace: 'location',
|
2016-12-11 19:35:29 +01:00
|
|
|
state: mutate(createLocation(), { params: {} }),
|
|
|
|
|
subscriptions: createSubscriptions(opts),
|
|
|
|
|
effects: { set: setLocation, touch: touchLocation },
|
|
|
|
|
reducers: { update: updateLocation }
|
2016-06-27 01:04:21 +02:00
|
|
|
}
|
2016-05-16 04:09:02 +07:00
|
|
|
|
2016-12-11 19:35:29 +01:00
|
|
|
// update the location on the state
|
|
|
|
|
// try and jump to an anchor on the page if it exists
|
|
|
|
|
// (obj, obj) -> obj
|
|
|
|
|
function updateLocation (state, data) {
|
|
|
|
|
if (opts.history !== false && data.hash && data.hash !== state.hash) {
|
|
|
|
|
try {
|
|
|
|
|
const el = document.querySelector(data.hash)
|
|
|
|
|
if (el) el.scrollIntoView(true)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return data
|
|
|
|
|
}
|
2016-06-27 01:04:21 +02:00
|
|
|
}
|
2016-12-11 19:35:29 +01:00
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update internal location only
|
|
|
|
|
// (str, obj, fn, fn) -> null
|
|
|
|
|
function touchLocation (state, data, send, done) {
|
|
|
|
|
const newLocation = createLocation(state, data)
|
|
|
|
|
send('location:update', newLocation, done)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set a new location e.g. "/foo/bar#baz?beep=boop"
|
|
|
|
|
// (str, obj, fn, fn) -> null
|
|
|
|
|
function setLocation (state, data, send, done) {
|
|
|
|
|
const newLocation = createLocation(state, data)
|
|
|
|
|
|
|
|
|
|
// update url bar if it changed
|
|
|
|
|
if (opts.history !== false && newLocation.href !== state.href) {
|
|
|
|
|
window.history.pushState({}, null, newLocation.href)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
send('location:update', newLocation, done)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSubscriptions (opts) {
|
|
|
|
|
const subs = {}
|
|
|
|
|
|
|
|
|
|
if (opts.history !== false) {
|
|
|
|
|
subs.handleHistory = function (send, done) {
|
|
|
|
|
onHistoryChange(function navigate (href) {
|
|
|
|
|
send('location:touch', href, done)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opts.href !== false) {
|
|
|
|
|
subs.handleHref = function (send, done) {
|
|
|
|
|
onHref(function navigate (location) {
|
|
|
|
|
send('location:set', location, done)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return subs
|
2016-05-11 13:51:17 +07:00
|
|
|
}
|
|
|
|
|
}
|