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:
Yoshua Wuyts
2017-06-28 15:53:37 +02:00
committed by GitHub
parent fc92f8eaad
commit eb9b7fd59f
5 changed files with 304 additions and 146 deletions
+149 -106
View File
@@ -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