Files
buuh/index.js
T

217 lines
6.0 KiB
JavaScript

const createLocation = require('sheet-router/create-location')
const onHistoryChange = require('sheet-router/history')
const sheetRouter = require('sheet-router')
const onHref = require('sheet-router/href')
const walk = require('sheet-router/walk')
const mutate = require('xtend/mutable')
const barracks = require('barracks')
const nanoraf = require('nanoraf')
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()
var _router = start._router = null
var _routerOpts = null
var _rootNode = null
var _routes = null
var _frame = null
if (typeof window !== 'undefined') {
_store.use({ onStateChange: render })
}
_store.use(opts)
start.toString = toString
start.router = router
start.model = model
start.start = start
start.stop = _store.stop
start.use = use
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({ subscriptions: false, reducers: false, effects: false })
const state = _store.state({ state: serverState })
const router = createRouter(_routerOpts, _routes, createSend)
const tree = router(route, state)
return tree.outerHTML || tree.toString()
function createSend () {
return function send () {
assert.ok(false, 'choo: send() cannot be called from Node')
}
}
}
// start the application
// (str?, obj?) -> DOMNode
function start () {
_store.model(createLocationModel(opts))
const createSend = _store.start(opts)
_router = start._router = createRouter(_routerOpts, _routes, createSend)
const state = _store.state({state: {}})
const tree = _router(state.location.href, state)
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')
_rootNode = tree
tree.done = done
return tree
// allow a 'mount' function to return the new node
// html -> null
function done (newNode) {
_rootNode = newNode
}
}
// update the DOM after every state mutation
// (obj, obj, obj, str, fn) -> null
function render (state, data, prev, name, createSend) {
if (!_frame) {
_frame = nanoraf(function (state, prev) {
const newTree = _router(state.location.href, state, prev)
_rootNode = yo.update(_rootNode, newTree)
})
}
_frame(state, prev)
}
// register all routes on the router
// (str?, [fn|[fn]]) -> obj
function router (defaultRoute, routes) {
_routerOpts = defaultRoute
_routes = routes
}
// create a new model
// (str?, obj) -> null
function model (model) {
_store.model(model)
}
// register a plugin
// (obj) -> null
function use (hooks) {
assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object')
_store.use(hooks)
}
// create a new router with a custom `createRoute()` function
// (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)
return router
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 })
const nwPrev = prev
prev = nwState // save for next time
if (opts.freeze !== false) Object.freeze(nwState)
return handler(nwState, nwPrev, send)
}
}
}
}
}
// application location model
// obj -> obj
function createLocationModel (opts) {
return {
namespace: 'location',
state: mutate(createLocation(), { params: {} }),
subscriptions: createSubscriptions(opts),
effects: { set: setLocation, touch: touchLocation },
reducers: { update: updateLocation }
}
// 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
}
}
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
}
}