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 ()
}