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 _store.use({ onStateChange: render }) _store.use(opts) start.toString = toString start.router = router start.model = model start.start = start 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 } }