var scrollToAnchor = require('scroll-to-anchor') var documentReady = require('document-ready') var nanolocation = require('nanolocation') var nanotiming = require('nanotiming') var nanorouter = require('nanorouter') var nanomorph = require('nanomorph') var nanoquery = require('nanoquery') var nanohref = require('nanohref') var nanoraf = require('nanoraf') var nanobus = require('nanobus') var assert = require('assert') var xtend = require('xtend') 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 self = this // define events used by choo this._events = { DOMCONTENTLOADED: 'DOMContentLoaded', DOMTITLECHANGE: 'DOMTitleChange', 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._hasWindow = typeof window !== 'undefined' this._createLocation = nanolocation this._loaded = false this._tree = null // properties that are part of the API this.router = nanorouter({ curry: true }) this.emitter = nanobus('choo.emit') var events = { events: this._events } if (this.hasWindow) { this.state = window.initialState ? xtend(window.initialState, events) : events delete window.initialState } else { this.state = events } // listen for title changes; available even when calling .toString() if (this._hasWindow) this.state.title = document.title 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 }) } 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 msg = 'choo.use' msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg var endTiming = nanotiming(msg) 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.state.query = nanoquery(window.location.search) if (self._loaded) { 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) }) } } this.state.href = this._createLocation() this.state.query = nanoquery(window.location.search) this._tree = this.router(this.state.href) assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href) this.emitter.prependListener(self._events.RENDER, nanoraf(function () { var renderTiming = nanotiming('choo.render') 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) 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) self._loaded = true }) 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 = xtend(this.state, state || {}) 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') 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') this.state.href = location.replace(/\?.+$/, '') this.state.query = nanoquery(location) var html = this.router(location) assert.ok(html, 'choo.toString: no valid value returned for the route ' + location) return html.toString() }