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-08-01 02:08:12 +02:00
this . _loaded = false
2018-03-07 13:20:47 +01:00
this . _stores = []
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
2018-01-17 23:58:42 +01:00
this . router = nanorouter ()
2017-06-28 15:53:37 +02:00
this . emitter = nanobus ( 'choo.emit' )
2018-01-17 23:58:42 +01:00
this . emit = this . emitter . emit . bind ( this . emitter )
2017-10-20 16:30:58 +02:00
var events = { events : this . _events }
2017-10-22 15:24:19 +02:00
if ( this . _hasWindow ) {
2017-10-20 16:30:58 +02:00
this . state = window . initialState
? xtend ( window . initialState , events )
: events
delete window . initialState
} else {
this . state = 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' )
2018-01-17 23:58:42 +01:00
this . router . on ( route , handler )
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' )
2018-03-07 13:20:47 +01:00
var self = this
this . _stores . push ( function () {
var msg = 'choo.use'
msg = cb . storeName ? msg + '(' + cb . storeName + ')' : msg
var endTiming = nanotiming ( msg )
cb ( self . state , self . emitter , self )
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 () {
2018-01-17 23:58:42 +01:00
self . _matchRoute ()
2017-08-01 02:08:12 +02:00
if ( self . _loaded ) {
self . emitter . emit ( self . _events . RENDER )
setTimeout ( scrollToAnchor . bind ( null , window . location . hash ), 0 )
}
2017-06-28 15:53:37 +02:00
})
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 )
})
}
}
2018-03-07 13:20:47 +01:00
this . _stores . forEach ( function ( initStore ) {
initStore ()
})
2018-01-17 23:58:42 +01:00
this . _matchRoute ()
this . _tree = this . _prerender ( this . state )
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' )
2018-01-17 23:58:42 +01:00
var newTree = self . _prerender ( self . state )
2017-07-26 19:31:15 +02:00
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 )
2017-08-01 02:08:12 +02:00
self . _loaded = true
2017-06-28 15:53:37 +02:00
})
return this . _tree
}
Choo . prototype . mount = function mount ( selector ) {
2018-02-13 22:09:08 +01:00
if ( typeof window !== 'object' ) {
assert . ok ( typeof selector === 'string' , 'choo.mount: selector should be type String' )
this . selector = selector
return this
}
2017-11-15 12:44:45 +01:00
assert . ok ( typeof selector === 'string' || typeof selector === 'object' , 'choo.mount: selector should be type String or HTMLElement' )
2017-06-28 15:53:37 +02:00
var self = this
documentReady ( function () {
var renderTiming = nanotiming ( 'choo.render' )
var newTree = self . start ()
2017-11-15 12:44:45 +01:00
if ( typeof selector === 'string' ) {
self . _tree = document . querySelector ( selector )
} else {
self . _tree = selector
}
2017-06-28 15:53:37 +02:00
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' )
2018-03-07 13:20:47 +01:00
// TODO: pass custom state down to each store.
this . _stores . forEach ( function ( initStore ) {
initStore ()
})
2018-01-17 23:58:42 +01:00
this . _matchRoute ( location )
var html = this . _prerender ( this . state )
2017-06-28 15:53:37 +02:00
assert . ok ( html , 'choo.toString: no valid value returned for the route ' + location )
2018-03-14 12:21:05 +01:00
assert ( ! Array . isArray ( html ), 'choo.toString: return value was an array for the route ' + location )
2018-03-05 08:11:07 -08:00
return typeof html . outerHTML === 'string' ? html . outerHTML : html . toString ()
2017-06-28 15:53:37 +02:00
}
2018-01-17 23:58:42 +01:00
Choo . prototype . _matchRoute = function ( locationOverride ) {
var location , queryString
if ( locationOverride ) {
location = locationOverride . replace ( /\?.+$/ , '' )
queryString = locationOverride
} else {
location = this . _createLocation ()
queryString = window . location . search
}
var matched = this . router . match ( location )
this . state . href = location
this . state . query = nanoquery ( queryString )
this . state . route = matched . route
this . state . params = matched . params
this . state . _handler = matched . cb
return this . state
}
Choo . prototype . _prerender = function ( state ) {
var routeTiming = nanotiming ( "choo.prerender('" + state . route + "')" )
var res = state . _handler ( state , this . emit )
routeTiming ()
return res
}