* become prototype based upgrade to new nanobus remove nanohistory add nanotiming remove nanomount move newTree to next tick upgrade timing API add more node warnings to .start/.mount add morph timing pass app instance to .use() self-clearing async-timing clean up timings better mount error fix mount bug de-prototype helper functions update to latest nanotiming upgrade to nanotiming 5 update nanotiming add toString asserts more assertions add event based tracing expose createlocation assert return types from render to always be the same add navigate event expose route on state expose events update uglify use scroll-to-anchor update nanobus document events update docs more docs changelog v6 upgrade nanomorph fixup! neatify morph assertions update bel dep add popstate docs to changelog * expose events on state * upgrade deps
182 lines
5.8 KiB
JavaScript
182 lines
5.8 KiB
JavaScript
var scrollToAnchor = require('scroll-to-anchor')
|
|
var documentReady = require('document-ready')
|
|
var nanotiming = require('nanotiming')
|
|
var nanorouter = require('nanorouter')
|
|
var nanomorph = require('nanomorph')
|
|
var nanohref = require('nanohref')
|
|
var nanoraf = require('nanoraf')
|
|
var nanobus = require('nanobus')
|
|
var assert = require('assert')
|
|
|
|
module.exports = Choo
|
|
|
|
var HISTORY_OBJECT = {}
|
|
|
|
function Choo (opts) {
|
|
if (!(this instanceof Choo)) return new Choo(opts)
|
|
opts = opts || {}
|
|
|
|
assert.equal(typeof opts, 'object', 'choo: opts should be type object')
|
|
|
|
var routerOpts = {
|
|
default: opts.defaultRoute || '/404',
|
|
curry: true
|
|
}
|
|
|
|
// define events used by choo
|
|
this._events = {
|
|
DOMCONTENTLOADED: 'DOMContentLoaded',
|
|
REPLACESTATE: 'replaceState',
|
|
PUSHSTATE: 'pushState',
|
|
NAVIGATE: 'navigate',
|
|
POPSTATE: 'popState',
|
|
RENDER: 'render'
|
|
}
|
|
|
|
// properties for internal use only
|
|
this._historyEnabled = opts.history === undefined ? true : opts.history
|
|
this._hrefEnabled = opts.href === undefined ? true : opts.href
|
|
this._tree = null
|
|
|
|
// properties that are part of the API
|
|
this.router = nanorouter(routerOpts)
|
|
this.emitter = nanobus('choo.emit')
|
|
this.state = { events: this._events }
|
|
}
|
|
|
|
Choo.prototype.route = function (route, handler) {
|
|
assert.equal(typeof route, 'string', 'choo.route: route should be type string')
|
|
assert.equal(typeof handler, 'function', 'choo.handler: route should be type function')
|
|
|
|
var self = this
|
|
this.router.on(route, function (params) {
|
|
return function () {
|
|
self.state.params = params
|
|
self.state.route = route
|
|
var routeTiming = nanotiming("choo.route('" + route + "')")
|
|
var res = handler(self.state, function (eventName, data) {
|
|
self.emitter.emit(eventName, data)
|
|
})
|
|
routeTiming()
|
|
return res
|
|
}
|
|
})
|
|
}
|
|
|
|
Choo.prototype.use = function (cb) {
|
|
assert.equal(typeof cb, 'function', 'choo.use: cb should be type function')
|
|
var endTiming = nanotiming('choo.use')
|
|
cb(this.state, this.emitter, this)
|
|
endTiming()
|
|
}
|
|
|
|
Choo.prototype.start = function () {
|
|
assert.equal(typeof window, 'object', 'choo.start: window was not found. .start() must be called in a browser, use .toString() if running in Node')
|
|
|
|
var self = this
|
|
|
|
if (this._historyEnabled) {
|
|
this.emitter.prependListener(this._events.NAVIGATE, function () {
|
|
self.emitter.emit(self._events.RENDER)
|
|
setTimeout(scrollToAnchor.bind(null, window.location.hash), 0)
|
|
})
|
|
|
|
this.emitter.prependListener(this._events.POPSTATE, function () {
|
|
self.emitter.emit(self._events.NAVIGATE)
|
|
})
|
|
|
|
this.emitter.prependListener(this._events.PUSHSTATE, function (href) {
|
|
assert.equal(typeof href, 'string', 'events.pushState: href should be type string')
|
|
window.history.pushState(HISTORY_OBJECT, null, href)
|
|
self.emitter.emit(self._events.NAVIGATE)
|
|
})
|
|
|
|
this.emitter.prependListener(this._events.REPLACESTATE, function (href) {
|
|
assert.equal(typeof href, 'string', 'events.replaceState: href should be type string')
|
|
window.history.replaceState(HISTORY_OBJECT, null, href)
|
|
self.emitter.emit(self._events.NAVIGATE)
|
|
})
|
|
|
|
window.onpopstate = function () {
|
|
self.emitter.emit(self._events.POPSTATE)
|
|
}
|
|
|
|
if (self._hrefEnabled) {
|
|
nanohref(function (location) {
|
|
var href = location.href
|
|
var currHref = window.location.href
|
|
if (href === currHref) return
|
|
self.emitter.emit(self._events.PUSHSTATE, href)
|
|
})
|
|
}
|
|
}
|
|
|
|
var location = this._createLocation()
|
|
this._tree = this.router(location)
|
|
assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + location)
|
|
|
|
this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
|
|
var renderTiming = nanotiming('choo.render')
|
|
|
|
var newTree = self.router(self._createLocation())
|
|
assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + location)
|
|
|
|
assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.render: The target node <' +
|
|
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
|
|
newTree.nodeName.toLowerCase() + '>.')
|
|
|
|
var morphTiming = nanotiming('choo.morph')
|
|
nanomorph(self._tree, newTree)
|
|
morphTiming()
|
|
|
|
renderTiming()
|
|
}))
|
|
|
|
documentReady(function () {
|
|
self.emitter.emit(self._events.DOMCONTENTLOADED)
|
|
})
|
|
|
|
return this._tree
|
|
}
|
|
|
|
Choo.prototype.mount = function mount (selector) {
|
|
assert.equal(typeof window, 'object', 'choo.mount: window was not found. .mount() must be called in a browser, use .toString() if running in Node')
|
|
assert.equal(typeof selector, 'string', 'choo.mount: selector should be type string')
|
|
|
|
var self = this
|
|
|
|
documentReady(function () {
|
|
var renderTiming = nanotiming('choo.render')
|
|
var newTree = self.start()
|
|
|
|
self._tree = document.querySelector(selector)
|
|
assert.ok(self._tree, 'choo.mount: could not query selector: ' + selector)
|
|
assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.mount: The target node <' +
|
|
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
|
|
newTree.nodeName.toLowerCase() + '>.')
|
|
|
|
var morphTiming = nanotiming('choo.morph')
|
|
nanomorph(self._tree, newTree)
|
|
morphTiming()
|
|
|
|
renderTiming()
|
|
})
|
|
}
|
|
|
|
Choo.prototype.toString = function (location, state) {
|
|
this.state = state || {}
|
|
|
|
assert.equal(typeof location, 'string', 'choo.toString: location should be type string')
|
|
assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object')
|
|
|
|
var html = this.router(location)
|
|
assert.ok(html, 'choo.toString: no valid value returned for the route ' + location)
|
|
return html.toString()
|
|
}
|
|
|
|
Choo.prototype._createLocation = function () {
|
|
var pathname = window.location.pathname.replace(/\/$/, '')
|
|
var hash = window.location.hash.replace(/^#/, '/')
|
|
return pathname + hash
|
|
}
|