Files
buuh/index.js
T

192 lines
6.6 KiB
JavaScript
Raw Normal View History

2017-06-28 15:53:37 +02:00
var scrollToAnchor = require('scroll-to-anchor')
2017-03-30 12:20:57 +02:00
var documentReady = require('document-ready')
2017-07-01 13:50:14 +02:00
var nanolocation = require('nanolocation')
2017-06-28 15:53:37 +02:00
var nanotiming = require('nanotiming')
2017-03-21 03:00:45 +01:00
var nanorouter = require('nanorouter')
var nanomorph = require('nanomorph')
2017-06-29 17:20:00 +02:00
var nanoquery = require('nanoquery')
2017-04-14 17:10:21 +02:00
var nanohref = require('nanohref')
2017-03-21 03:00:45 +01:00
var nanoraf = require('nanoraf')
var nanobus = require('nanobus')
var assert = require('assert')
2017-07-11 15:07:53 +02:00
var xtend = require('xtend')
2016-05-10 23:23:33 +07:00
2017-04-02 01:19:59 +02:00
module.exports = Choo
2017-03-21 03:00:45 +01:00
2017-06-28 15:53:37 +02:00
var HISTORY_OBJECT = {}
2017-04-02 01:19:59 +02:00
function Choo (opts) {
2017-06-28 15:53:37 +02:00
if (!(this instanceof Choo)) return new Choo(opts)
2016-06-27 01:04:21 +02:00
opts = opts || {}
2017-06-28 15:53:37 +02:00
assert.equal(typeof opts, 'object', 'choo: opts should be type object')
2017-07-03 10:43:44 +02:00
var self = this
2017-06-28 15:53:37 +02:00
// define events used by choo
this._events = {
DOMCONTENTLOADED: 'DOMContentLoaded',
2017-07-03 10:43:44 +02:00
DOMTITLECHANGE: 'DOMTitleChange',
2017-06-28 15:53:37 +02:00
REPLACESTATE: 'replaceState',
PUSHSTATE: 'pushState',
NAVIGATE: 'navigate',
POPSTATE: 'popState',
RENDER: 'render'
2016-06-27 01:04:21 +02:00
}
2016-05-16 04:09:02 +07:00
2017-06-28 15:53:37 +02:00
// properties for internal use only
this._historyEnabled = opts.history === undefined ? true : opts.history
this._hrefEnabled = opts.href === undefined ? true : opts.href
2017-07-03 10:43:44 +02:00
this._hasWindow = typeof window !== 'undefined'
2017-07-01 13:50:14 +02:00
this._createLocation = nanolocation
2017-06-28 15:53:37 +02:00
this._tree = null
2016-12-11 19:35:29 +01:00
2017-06-28 15:53:37 +02:00
// properties that are part of the API
2017-06-29 17:20:00 +02:00
this.router = nanorouter({ curry: true })
2017-06-28 15:53:37 +02:00
this.emitter = nanobus('choo.emit')
this.state = { events: this._events }
2017-07-03 10:43:44 +02:00
// listen for title changes; available even when calling .toString()
2017-07-11 15:07:53 +02:00
if (this._hasWindow) this.state.title = document.title
2017-07-03 10:43:44 +02:00
this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) {
assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string')
self.state.title = title
if (self._hasWindow) document.title = title
})
2017-06-28 15:53:37 +02:00
}
2016-12-11 19:35:29 +01:00
2017-06-28 15:53:37 +02:00
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)
2017-03-22 20:42:53 +01:00
})
2017-06-28 15:53:37 +02:00
routeTiming()
return res
2016-12-11 19:35:29 +01:00
}
2017-06-28 15:53:37 +02:00
})
2016-05-11 13:51:17 +07:00
}
2017-03-21 03:00:45 +01:00
2017-06-28 15:53:37 +02:00
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()
2017-03-21 03:00:45 +01:00
}
2017-06-28 15:53:37 +02:00
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)
2017-06-29 17:20:00 +02:00
self.state.query = nanoquery(window.location.search)
2017-06-28 15:53:37 +02:00
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)
})
}
}
2017-07-26 19:31:15 +02:00
this.state.href = this._createLocation()
this._tree = this.router(this.state.href)
2017-06-29 17:20:00 +02:00
this.state.query = nanoquery(window.location.search)
2017-07-26 19:31:15 +02:00
assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href)
2017-06-28 15:53:37 +02:00
this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
var renderTiming = nanotiming('choo.render')
2017-07-26 19:31:15 +02:00
self.state.href = self._createLocation()
var newTree = self.router(self.state.href)
assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href)
2017-06-28 15:53:37 +02:00
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) {
2017-07-27 03:32:05 +10:00
this.state = xtend(this.state, state || {})
2017-06-28 15:53:37 +02:00
2017-07-11 15:07:53 +02:00
assert.notEqual(typeof window, 'object', 'choo.mount: window was found. .toString() must be called in Node, use .start() or .mount() if running in the browser')
2017-06-28 15:53:37 +02:00
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')
2017-07-26 19:31:15 +02:00
this.state.href = location.replace(/\?*.$/, '')
2017-06-29 17:20:00 +02:00
this.state.query = nanoquery(location)
2017-06-28 15:53:37 +02:00
var html = this.router(location)
assert.ok(html, 'choo.toString: no valid value returned for the route ' + location)
return html.toString()
}