v6 beta branch (#489)
* 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
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
var scrollToAnchor = require('scroll-to-anchor')
|
||||
var documentReady = require('document-ready')
|
||||
var nanohistory = require('nanohistory')
|
||||
var nanotiming = require('nanotiming')
|
||||
var nanorouter = require('nanorouter')
|
||||
var nanomount = require('nanomount')
|
||||
var nanomorph = require('nanomorph')
|
||||
var nanohref = require('nanohref')
|
||||
var nanoraf = require('nanoraf')
|
||||
@@ -10,128 +10,171 @@ 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
|
||||
}
|
||||
|
||||
var timingEnabled = opts.timing === undefined ? true : opts.timing
|
||||
var hasWindow = typeof window !== 'undefined'
|
||||
var hasPerformance = hasWindow && window.performance && window.performance.mark
|
||||
var router = nanorouter(routerOpts)
|
||||
var bus = nanobus()
|
||||
var rerender = null
|
||||
var tree = null
|
||||
var state = {}
|
||||
|
||||
return {
|
||||
toString: toString,
|
||||
use: register,
|
||||
mount: mount,
|
||||
router: router,
|
||||
route: route,
|
||||
start: start
|
||||
// define events used by choo
|
||||
this._events = {
|
||||
DOMCONTENTLOADED: 'DOMContentLoaded',
|
||||
REPLACESTATE: 'replaceState',
|
||||
PUSHSTATE: 'pushState',
|
||||
NAVIGATE: 'navigate',
|
||||
POPSTATE: 'popState',
|
||||
RENDER: 'render'
|
||||
}
|
||||
|
||||
function route (route, handler) {
|
||||
router.on(route, function (params) {
|
||||
return function () {
|
||||
state.params = params
|
||||
return handler(state, emit)
|
||||
}
|
||||
})
|
||||
}
|
||||
// properties for internal use only
|
||||
this._historyEnabled = opts.history === undefined ? true : opts.history
|
||||
this._hrefEnabled = opts.href === undefined ? true : opts.href
|
||||
this._tree = null
|
||||
|
||||
function register (cb) {
|
||||
cb(state, bus)
|
||||
}
|
||||
// properties that are part of the API
|
||||
this.router = nanorouter(routerOpts)
|
||||
this.emitter = nanobus('choo.emit')
|
||||
this.state = { events: this._events }
|
||||
}
|
||||
|
||||
function start () {
|
||||
if (opts.history !== false) {
|
||||
nanohistory(function (href) {
|
||||
bus.emit('pushState')
|
||||
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)
|
||||
})
|
||||
|
||||
bus.prependListener('pushState', updateHistory.bind(null, 'push'))
|
||||
bus.prependListener('replaceState', updateHistory.bind(null, 'replace'))
|
||||
|
||||
if (opts.href !== false) {
|
||||
nanohref(function (location) {
|
||||
var href = location.href
|
||||
var currHref = window.location.href
|
||||
if (href === currHref) return
|
||||
bus.emit('pushState', href)
|
||||
})
|
||||
}
|
||||
routeTiming()
|
||||
return res
|
||||
}
|
||||
|
||||
function updateHistory (mode, href) {
|
||||
if (href) window.history[mode + 'State']({}, null, href)
|
||||
bus.emit('render')
|
||||
setTimeout(function () {
|
||||
scrollIntoView()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
rerender = nanoraf(function () {
|
||||
if (hasPerformance && timingEnabled) {
|
||||
window.performance.mark('choo:renderStart')
|
||||
}
|
||||
var newTree = router(createLocation())
|
||||
tree = nanomorph(tree, newTree)
|
||||
assert.notEqual(tree, newTree, 'choo.start: a different node type was returned as the root node on a rerender. Make sure that the root node is always the same type to prevent the application from being unmounted.')
|
||||
if (hasPerformance && timingEnabled) {
|
||||
window.performance.mark('choo:renderEnd')
|
||||
window.performance.measure('choo:render', 'choo:renderStart', 'choo:renderEnd')
|
||||
}
|
||||
})
|
||||
|
||||
bus.prependListener('render', rerender)
|
||||
|
||||
documentReady(function () {
|
||||
bus.emit('DOMContentLoaded')
|
||||
})
|
||||
|
||||
tree = router(createLocation())
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
function emit (eventName, data) {
|
||||
bus.emit(eventName, data)
|
||||
}
|
||||
|
||||
function mount (selector) {
|
||||
var newTree = start()
|
||||
documentReady(function () {
|
||||
var root = document.querySelector(selector)
|
||||
assert.ok(root, 'choo.mount: could not query selector: ' + selector)
|
||||
nanomount(root, newTree)
|
||||
tree = root
|
||||
})
|
||||
}
|
||||
|
||||
function toString (location, _state) {
|
||||
state = _state || {}
|
||||
var html = router(location)
|
||||
return html.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function scrollIntoView () {
|
||||
var hash = window.location.hash
|
||||
if (hash) {
|
||||
try {
|
||||
var el = document.querySelector(hash)
|
||||
if (el) el.scrollIntoView(true)
|
||||
} catch (e) {}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
function createLocation () {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user