2015-04-21 19:38:17 +02:00
/* jshint -W058 */
2015-04-29 11:02:32 +02:00
'use strict' ;
var React = require ( 'react' ) ;
var warning = require ( 'react/lib/warning' ) ;
var invariant = require ( 'react/lib/invariant' ) ;
var canUseDOM = require ( 'react/lib/ExecutionEnvironment' ) . canUseDOM ;
var LocationActions = require ( './actions/LocationActions' ) ;
var ImitateBrowserBehavior = require ( './behaviors/ImitateBrowserBehavior' ) ;
var HashLocation = require ( './locations/HashLocation' ) ;
var HistoryLocation = require ( './locations/HistoryLocation' ) ;
var RefreshLocation = require ( './locations/RefreshLocation' ) ;
var StaticLocation = require ( './locations/StaticLocation' ) ;
var ScrollHistory = require ( './ScrollHistory' ) ;
var createRoutesFromReactChildren = require ( './createRoutesFromReactChildren' ) ;
var isReactChildren = require ( './isReactChildren' ) ;
var Transition = require ( './Transition' ) ;
var PropTypes = require ( './PropTypes' ) ;
var Redirect = require ( './Redirect' ) ;
var History = require ( './History' ) ;
var Cancellation = require ( './Cancellation' ) ;
var Match = require ( './Match' ) ;
var Route = require ( './Route' ) ;
var supportsHistory = require ( './supportsHistory' ) ;
var PathUtils = require ( './PathUtils' ) ;
2015-04-21 19:38:17 +02:00
/ * *
* The default location for new routers .
* /
2015-04-29 11:02:32 +02:00
var DEFAULT _LOCATION = canUseDOM ? HashLocation : '/' ;
2015-04-21 19:38:17 +02:00
/ * *
* The default scroll behavior for new routers .
* /
var DEFAULT _SCROLL _BEHAVIOR = canUseDOM ? ImitateBrowserBehavior : null ;
function hasProperties ( object , properties ) {
for ( var propertyName in properties ) if ( properties . hasOwnProperty ( propertyName ) && object [ propertyName ] !== properties [ propertyName ] ) {
return false ;
} return true ;
}
function hasMatch ( routes , route , prevParams , nextParams , prevQuery , nextQuery ) {
return routes . some ( function ( r ) {
if ( r !== route ) return false ;
var paramNames = route . paramNames ;
var paramName ;
// Ensure that all params the route cares about did not change.
for ( var i = 0 , len = paramNames . length ; i < len ; ++ i ) {
paramName = paramNames [ i ] ;
if ( nextParams [ paramName ] !== prevParams [ paramName ] ) return false ;
}
// Ensure the query hasn't changed.
return hasProperties ( prevQuery , nextQuery ) && hasProperties ( nextQuery , prevQuery ) ;
} ) ;
}
function addRoutesToNamedRoutes ( routes , namedRoutes ) {
var route ;
for ( var i = 0 , len = routes . length ; i < len ; ++ i ) {
route = routes [ i ] ;
if ( route . name ) {
2015-04-29 11:02:32 +02:00
invariant ( namedRoutes [ route . name ] == null , 'You may not have more than one route named "%s"' , route . name ) ;
2015-04-21 19:38:17 +02:00
namedRoutes [ route . name ] = route ;
}
if ( route . childRoutes ) addRoutesToNamedRoutes ( route . childRoutes , namedRoutes ) ;
}
}
function routeIsActive ( activeRoutes , routeName ) {
return activeRoutes . some ( function ( route ) {
return route . name === routeName ;
} ) ;
}
function paramsAreActive ( activeParams , params ) {
for ( var property in params ) if ( String ( activeParams [ property ] ) !== String ( params [ property ] ) ) {
return false ;
} return true ;
}
function queryIsActive ( activeQuery , query ) {
for ( var property in query ) if ( String ( activeQuery [ property ] ) !== String ( query [ property ] ) ) {
return false ;
} return true ;
}
/ * *
* Creates and returns a new router using the given options . A router
* is a ReactComponent class that knows how to react to changes in the
* URL and keep the contents of the page in sync .
*
* Options may be any of the following :
*
* - routes ( required ) The route config
* - location The location to use . Defaults to HashLocation when
* the DOM is available , "/" otherwise
* - scrollBehavior The scroll behavior to use . Defaults to ImitateBrowserBehavior
* when the DOM is available , null otherwise
* - onError A function that is used to handle errors
* - onAbort A function that is used to handle aborted transitions
*
* When rendering in a server - side environment , the location should simply
* be the URL path that was used in the request , including the query string .
* /
function createRouter ( options ) {
options = options || { } ;
if ( isReactChildren ( options ) ) options = { routes : options } ;
var mountedComponents = [ ] ;
var location = options . location || DEFAULT _LOCATION ;
var scrollBehavior = options . scrollBehavior || DEFAULT _SCROLL _BEHAVIOR ;
var state = { } ;
var nextState = { } ;
var pendingTransition = null ;
var dispatchHandler = null ;
2015-04-29 11:02:32 +02:00
if ( typeof location === 'string' ) location = new StaticLocation ( location ) ;
2015-04-21 19:38:17 +02:00
if ( location instanceof StaticLocation ) {
2015-04-29 11:02:32 +02:00
warning ( ! canUseDOM || process . env . NODE _ENV === 'test' , 'You should not use a static location in a DOM environment because ' + 'the router will not be kept in sync with the current URL' ) ;
2015-04-21 19:38:17 +02:00
} else {
2015-04-29 11:02:32 +02:00
invariant ( canUseDOM || location . needsDOM === false , 'You cannot use %s without a DOM' , location ) ;
2015-04-21 19:38:17 +02:00
}
// Automatically fall back to full page refreshes in
// browsers that don't support the HTML history API.
if ( location === HistoryLocation && ! supportsHistory ( ) ) location = RefreshLocation ;
var Router = React . createClass ( {
2015-04-29 11:02:32 +02:00
displayName : 'Router' ,
2015-04-21 19:38:17 +02:00
statics : {
isRunning : false ,
cancelPendingTransition : function cancelPendingTransition ( ) {
if ( pendingTransition ) {
pendingTransition . cancel ( ) ;
pendingTransition = null ;
}
} ,
clearAllRoutes : function clearAllRoutes ( ) {
Router . cancelPendingTransition ( ) ;
Router . namedRoutes = { } ;
Router . routes = [ ] ;
} ,
/ * *
* Adds routes to this router from the given children object ( see ReactChildren ) .
* /
addRoutes : function addRoutes ( routes ) {
if ( isReactChildren ( routes ) ) routes = createRoutesFromReactChildren ( routes ) ;
addRoutesToNamedRoutes ( routes , Router . namedRoutes ) ;
Router . routes . push . apply ( Router . routes , routes ) ;
} ,
/ * *
* Replaces routes of this router from the given children object ( see ReactChildren ) .
* /
replaceRoutes : function replaceRoutes ( routes ) {
Router . clearAllRoutes ( ) ;
Router . addRoutes ( routes ) ;
Router . refresh ( ) ;
} ,
/ * *
* Performs a match of the given path against this router and returns an object
* with the { routes , params , pathname , query } that match . Returns null if no
* match can be made .
* /
match : function match ( path ) {
return Match . findMatch ( Router . routes , path ) ;
} ,
/ * *
* Returns an absolute URL path created from the given route
* name , URL parameters , and query .
* /
makePath : function makePath ( to , params , query ) {
var path ;
if ( PathUtils . isAbsolute ( to ) ) {
path = to ;
} else {
var route = to instanceof Route ? to : Router . namedRoutes [ to ] ;
2015-04-29 11:02:32 +02:00
invariant ( route instanceof Route , 'Cannot find a route named "%s"' , to ) ;
2015-04-21 19:38:17 +02:00
path = route . path ;
}
return PathUtils . withQuery ( PathUtils . injectParams ( path , params ) , query ) ;
} ,
/ * *
* Returns a string that may safely be used as the href of a link
* to the route with the given name , URL parameters , and query .
* /
makeHref : function makeHref ( to , params , query ) {
var path = Router . makePath ( to , params , query ) ;
2015-04-29 11:02:32 +02:00
return location === HashLocation ? '#' + path : path ;
2015-04-21 19:38:17 +02:00
} ,
/ * *
* Transitions to the URL specified in the arguments by pushing
* a new URL onto the history stack .
* /
transitionTo : function transitionTo ( to , params , query ) {
var path = Router . makePath ( to , params , query ) ;
if ( pendingTransition ) {
// Replace so pending location does not stay in history.
location . replace ( path ) ;
} else {
location . push ( path ) ;
}
} ,
/ * *
* Transitions to the URL specified in the arguments by replacing
* the current URL in the history stack .
* /
replaceWith : function replaceWith ( to , params , query ) {
location . replace ( Router . makePath ( to , params , query ) ) ;
} ,
/ * *
* Transitions to the previous URL if one is available . Returns true if the
* router was able to go back , false otherwise .
*
* Note : The router only tracks history entries in your application , not the
* current browser session , so you can safely call this function without guarding
* against sending the user back to some other site . However , when using
* RefreshLocation ( which is the fallback for HistoryLocation in browsers that
* don ' t support HTML5 history ) this method will * always * send the client back
* because we cannot reliably track history length .
* /
goBack : function goBack ( ) {
if ( History . length > 1 || location === RefreshLocation ) {
location . pop ( ) ;
return true ;
}
2015-04-29 11:02:32 +02:00
warning ( false , 'goBack() was ignored because there is no router history' ) ;
2015-04-21 19:38:17 +02:00
return false ;
} ,
handleAbort : options . onAbort || function ( abortReason ) {
2015-04-29 11:02:32 +02:00
if ( location instanceof StaticLocation ) throw new Error ( 'Unhandled aborted transition! Reason: ' + abortReason ) ;
2015-04-21 19:38:17 +02:00
if ( abortReason instanceof Cancellation ) {
return ;
} else if ( abortReason instanceof Redirect ) {
location . replace ( Router . makePath ( abortReason . to , abortReason . params , abortReason . query ) ) ;
} else {
location . pop ( ) ;
}
} ,
handleError : options . onError || function ( error ) {
// Throw so we don't silently swallow async errors.
throw error ; // This error probably originated in a transition hook.
} ,
handleLocationChange : function handleLocationChange ( change ) {
Router . dispatch ( change . path , change . type ) ;
} ,
/ * *
* Performs a transition to the given path and calls callback ( error , abortReason )
* when the transition is finished . If both arguments are null the router ' s state
* was updated . Otherwise the transition did not complete .
*
* In a transition , a router first determines which routes are involved by beginning
* with the current route , up the route tree to the first parent route that is shared
* with the destination route , and back down the tree to the destination route . The
* willTransitionFrom hook is invoked on all route handlers we ' re transitioning away
* from , in reverse nesting order . Likewise , the willTransitionTo hook is invoked on
* all route handlers we ' re transitioning to .
*
* Both willTransitionFrom and willTransitionTo hooks may either abort or redirect the
* transition . To resolve asynchronously , they may use the callback argument . If no
* hooks wait , the transition is fully synchronous .
* /
dispatch : function dispatch ( path , action ) {
Router . cancelPendingTransition ( ) ;
var prevPath = state . path ;
var isRefreshing = action == null ;
if ( prevPath === path && ! isRefreshing ) {
return ;
} // Nothing to do!
// Record the scroll position as early as possible to
// get it before browsers try update it automatically.
if ( prevPath && action === LocationActions . PUSH ) Router . recordScrollPosition ( prevPath ) ;
var match = Router . match ( path ) ;
2015-04-29 11:02:32 +02:00
warning ( match != null , 'No route matches path "%s". Make sure you have <Route path="%s"> somewhere in your routes' , path , path ) ;
2015-04-21 19:38:17 +02:00
if ( match == null ) match = { } ;
var prevRoutes = state . routes || [ ] ;
var prevParams = state . params || { } ;
var prevQuery = state . query || { } ;
var nextRoutes = match . routes || [ ] ;
var nextParams = match . params || { } ;
var nextQuery = match . query || { } ;
var fromRoutes , toRoutes ;
if ( prevRoutes . length ) {
fromRoutes = prevRoutes . filter ( function ( route ) {
return ! hasMatch ( nextRoutes , route , prevParams , nextParams , prevQuery , nextQuery ) ;
} ) ;
toRoutes = nextRoutes . filter ( function ( route ) {
return ! hasMatch ( prevRoutes , route , prevParams , nextParams , prevQuery , nextQuery ) ;
} ) ;
} else {
fromRoutes = [ ] ;
toRoutes = nextRoutes ;
}
var transition = new Transition ( path , Router . replaceWith . bind ( Router , path ) ) ;
pendingTransition = transition ;
var fromComponents = mountedComponents . slice ( prevRoutes . length - fromRoutes . length ) ;
Transition . from ( transition , fromRoutes , fromComponents , function ( error ) {
if ( error || transition . abortReason ) return dispatchHandler . call ( Router , error , transition ) ; // No need to continue.
Transition . to ( transition , toRoutes , nextParams , nextQuery , function ( error ) {
dispatchHandler . call ( Router , error , transition , {
path : path ,
action : action ,
pathname : match . pathname ,
routes : nextRoutes ,
params : nextParams ,
query : nextQuery
} ) ;
} ) ;
} ) ;
} ,
/ * *
* Starts this router and calls callback ( router , state ) when the route changes .
*
* If the router ' s location is static ( i . e . a URL path in a server environment )
* the callback is called only once . Otherwise , the location should be one of the
* Router . * Location objects ( e . g . Router . HashLocation or Router . HistoryLocation ) .
* /
run : function run ( callback ) {
2015-04-29 11:02:32 +02:00
invariant ( ! Router . isRunning , 'Router is already running' ) ;
2015-04-21 19:38:17 +02:00
dispatchHandler = function ( error , transition , newState ) {
if ( error ) Router . handleError ( error ) ;
if ( pendingTransition !== transition ) return ;
pendingTransition = null ;
if ( transition . abortReason ) {
Router . handleAbort ( transition . abortReason ) ;
} else {
callback . call ( Router , Router , nextState = newState ) ;
}
} ;
if ( ! ( location instanceof StaticLocation ) ) {
if ( location . addChangeListener ) location . addChangeListener ( Router . handleLocationChange ) ;
Router . isRunning = true ;
}
// Bootstrap using the current path.
Router . refresh ( ) ;
} ,
refresh : function refresh ( ) {
Router . dispatch ( location . getCurrentPath ( ) , null ) ;
} ,
stop : function stop ( ) {
Router . cancelPendingTransition ( ) ;
if ( location . removeChangeListener ) location . removeChangeListener ( Router . handleLocationChange ) ;
Router . isRunning = false ;
} ,
getLocation : function getLocation ( ) {
return location ;
} ,
getScrollBehavior : function getScrollBehavior ( ) {
return scrollBehavior ;
} ,
getRouteAtDepth : function getRouteAtDepth ( routeDepth ) {
var routes = state . routes ;
return routes && routes [ routeDepth ] ;
} ,
setRouteComponentAtDepth : function setRouteComponentAtDepth ( routeDepth , component ) {
mountedComponents [ routeDepth ] = component ;
} ,
/ * *
* Returns the current URL path + query string .
* /
getCurrentPath : function getCurrentPath ( ) {
return state . path ;
} ,
/ * *
* Returns the current URL path without the query string .
* /
getCurrentPathname : function getCurrentPathname ( ) {
return state . pathname ;
} ,
/ * *
* Returns an object of the currently active URL parameters .
* /
getCurrentParams : function getCurrentParams ( ) {
return state . params ;
} ,
/ * *
* Returns an object of the currently active query parameters .
* /
getCurrentQuery : function getCurrentQuery ( ) {
return state . query ;
} ,
/ * *
* Returns an array of the currently active routes .
* /
getCurrentRoutes : function getCurrentRoutes ( ) {
return state . routes ;
} ,
/ * *
* Returns true if the given route , params , and query are active .
* /
isActive : function isActive ( to , params , query ) {
if ( PathUtils . isAbsolute ( to ) ) {
return to === state . path ;
} return routeIsActive ( state . routes , to ) && paramsAreActive ( state . params , params ) && ( query == null || queryIsActive ( state . query , query ) ) ;
}
} ,
mixins : [ ScrollHistory ] ,
propTypes : {
children : PropTypes . falsy
} ,
childContextTypes : {
routeDepth : PropTypes . number . isRequired ,
router : PropTypes . router . isRequired
} ,
getChildContext : function getChildContext ( ) {
return {
routeDepth : 1 ,
router : Router
} ;
} ,
getInitialState : function getInitialState ( ) {
return state = nextState ;
} ,
componentWillReceiveProps : function componentWillReceiveProps ( ) {
this . setState ( state = nextState ) ;
} ,
componentWillUnmount : function componentWillUnmount ( ) {
Router . stop ( ) ;
} ,
render : function render ( ) {
var route = Router . getRouteAtDepth ( 0 ) ;
return route ? React . createElement ( route . handler , this . props ) : null ;
}
} ) ;
Router . clearAllRoutes ( ) ;
if ( options . routes ) Router . addRoutes ( options . routes ) ;
return Router ;
}
module . exports = createRouter ;