Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| var scrollToAnchor = require('scroll-to-anchor') | |
| var documentReady = require('document-ready') | |
| 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 Cache = require('./component/cache') | |
| module.exports = Choo | |
| var HISTORY_OBJECT = {} | |
| function Choo (opts) { | |
| var timing = nanotiming('choo.constructor') | |
| 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._hashEnabled = opts.hash === undefined ? false : opts.hash | |
| this._hasWindow = typeof window !== 'undefined' | |
| this._cache = opts.cache | |
| this._loaded = false | |
| this._stores = [ondomtitlechange] | |
| this._tree = null | |
| // state | |
| var _state = { | |
| events: this._events, | |
| components: {} | |
| } | |
| if (this._hasWindow) { | |
| this.state = window.initialState | |
| ? Object.assign({}, window.initialState, _state) | |
| : _state | |
| delete window.initialState | |
| } else { | |
| this.state = _state | |
| } | |
| // properties that are part of the API | |
| this.router = nanorouter({ curry: true }) | |
| this.emitter = nanobus('choo.emit') | |
| this.emit = this.emitter.emit.bind(this.emitter) | |
| // listen for title changes; available even when calling .toString() | |
| if (this._hasWindow) this.state.title = document.title | |
| function ondomtitlechange (state) { | |
| self.emitter.prependListener(self._events.DOMTITLECHANGE, function (title) { | |
| assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string') | |
| state.title = title | |
| if (self._hasWindow) document.title = title | |
| }) | |
| } | |
| timing() | |
| } | |
| Choo.prototype.route = function (route, handler) { | |
| var routeTiming = nanotiming("choo.route('" + route + "')") | |
| assert.equal(typeof route, 'string', 'choo.route: route should be type string') | |
| assert.equal(typeof handler, 'function', 'choo.handler: route should be type function') | |
| this.router.on(route, handler) | |
| routeTiming() | |
| } | |
| Choo.prototype.use = function (cb) { | |
| assert.equal(typeof cb, 'function', 'choo.use: cb should be type function') | |
| var self = this | |
| this._stores.push(function (state) { | |
| var msg = 'choo.use' | |
| msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg | |
| var endTiming = nanotiming(msg) | |
| cb(state, self.emitter, self) | |
| 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 startTiming = nanotiming('choo.start') | |
| var self = this | |
| if (this._historyEnabled) { | |
| this.emitter.prependListener(this._events.NAVIGATE, function () { | |
| self._matchRoute(self.state) | |
| 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 hash = location.hash | |
| if (href === window.location.href) { | |
| if (!self._hashEnabled && hash) scrollToAnchor(hash) | |
| return | |
| } | |
| self.emitter.emit(self._events.PUSHSTATE, href) | |
| }) | |
| } | |
| } | |
| this._setCache(this.state) | |
| this._matchRoute(this.state) | |
| this._stores.forEach(function (initStore) { | |
| initStore(self.state) | |
| }) | |
| this._tree = this._prerender(this.state) | |
| 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') | |
| var newTree = self._prerender(self.state) | |
| 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 | |
| }) | |
| startTiming() | |
| return this._tree | |
| } | |
| Choo.prototype.mount = function mount (selector) { | |
| var mountTiming = nanotiming("choo.mount('" + selector + "')") | |
| if (typeof window !== 'object') { | |
| assert.ok(typeof selector === 'string', 'choo.mount: selector should be type String') | |
| this.selector = selector | |
| mountTiming() | |
| return this | |
| } | |
| assert.ok(typeof selector === 'string' || typeof selector === 'object', 'choo.mount: selector should be type String or HTMLElement') | |
| var self = this | |
| documentReady(function () { | |
| var renderTiming = nanotiming('choo.render') | |
| var newTree = self.start() | |
| if (typeof selector === 'string') { | |
| self._tree = document.querySelector(selector) | |
| } else { | |
| self._tree = 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() | |
| }) | |
| mountTiming() | |
| } | |
| Choo.prototype.toString = function (location, state) { | |
| state = state || {} | |
| state.components = state.components || {} | |
| state.events = Object.assign({}, state.events, this._events) | |
| 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 state, 'object', 'choo.toString: state should be type object') | |
| this._setCache(state) | |
| this._matchRoute(state, location) | |
| this.emitter.removeAllListeners() | |
| this._stores.forEach(function (initStore) { | |
| initStore(state) | |
| }) | |
| var html = this._prerender(state) | |
| assert.ok(html, 'choo.toString: no valid value returned for the route ' + location) | |
| assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location) | |
| return typeof html.outerHTML === 'string' ? html.outerHTML : html.toString() | |
| } | |
| Choo.prototype._matchRoute = function (state, locationOverride) { | |
| var location, queryString | |
| if (locationOverride) { | |
| location = locationOverride.replace(/\?.+$/, '').replace(/\/$/, '') | |
| if (!this._hashEnabled) location = location.replace(/#.+$/, '') | |
| queryString = locationOverride | |
| } else { | |
| location = window.location.pathname.replace(/\/$/, '') | |
| if (this._hashEnabled) location += window.location.hash.replace(/^#/, '/') | |
| queryString = window.location.search | |
| } | |
| var matched = this.router.match(location) | |
| this._handler = matched.cb | |
| state.href = location | |
| state.query = nanoquery(queryString) | |
| state.route = matched.route | |
| state.params = matched.params | |
| } | |
| Choo.prototype._prerender = function (state) { | |
| var routeTiming = nanotiming("choo.prerender('" + state.route + "')") | |
| var res = this._handler(state, this.emit) | |
| routeTiming() | |
| return res | |
| } | |
| Choo.prototype._setCache = function (state) { | |
| var cache = new Cache(state, this.emitter.emit.bind(this.emitter), this._cache) | |
| state.cache = renderComponent | |
| function renderComponent (Component, id) { | |
| assert.equal(typeof Component, 'function', 'choo.state.cache: Component should be type function') | |
| var args = [] | |
| for (var i = 0, len = arguments.length; i < len; i++) { | |
| args.push(arguments[i]) | |
| } | |
| return cache.render.apply(cache, args) | |
| } | |
| // When the state gets stringified, make sure `state.cache` isn't | |
| // stringified too. | |
| renderComponent.toJSON = function () { | |
| return null | |
| } | |
| } |