react-router
Advanced tools
Comparing version 0.9.3 to 0.9.4
@@ -0,5 +1,18 @@ | ||
v0.9.4 - Mon, 13 Oct 2014 19:53:10 GMT | ||
-------------------------------------- | ||
- [0cd20cf](../../commit/0cd20cf) Revert "[removed] "static" <Route> props" | ||
- [db26b7d](../../commit/db26b7d) [removed] "static" <Route> props | ||
- [e571c27](../../commit/e571c27) [fixed] Add .active class to <Link>s with absolute hrefs | ||
- [ea5a380](../../commit/ea5a380) [fixed] Make sure onChange is fired at synchronous first render | ||
- [dee374f](../../commit/dee374f) [fixed] Listen to path changes caused by initial redirect, fixes #360 | ||
- [d47d7dd](../../commit/d47d7dd) [fixed] potential infinite loop during transitions | ||
- [1b1a62b](../../commit/1b1a62b) [added] Server-side rendering | ||
- [c7ca87e](../../commit/c7ca87e) [added] <Routes onError> | ||
v0.9.3 - Wed, 08 Oct 2014 14:44:52 GMT | ||
-------------------------------------- | ||
- | ||
- [caf3a2b](../../commit/caf3a2b) [fixed] scrollBehavior='none' on path update | ||
@@ -6,0 +19,0 @@ |
@@ -32,3 +32,3 @@ API: `Routes` (component) | ||
- `'imitateBrowser'` - default, imitates what browsers do in a typical | ||
- `'browser'` - default, imitates what browsers do in a typical | ||
page reload scenario: preserves scroll positions when using the back | ||
@@ -43,2 +43,6 @@ button, scrolls up when you come to a new route by clicking a link, | ||
### `onError` | ||
Called when a transition throws an error. | ||
#### signature | ||
@@ -48,4 +52,3 @@ | ||
Example | ||
------- | ||
#### Example | ||
@@ -52,0 +55,0 @@ ```jsx |
@@ -29,3 +29,17 @@ API: `Router` | ||
Router.Transitions | ||
// methods | ||
Router.renderRoutesToString | ||
``` | ||
Methods | ||
------- | ||
### `renderRoutesToString(routes, path, callback)` | ||
We will document this more when the data loading story finalizes. | ||
### `renderRoutesToStaticMarkup(routes, path, callback)` | ||
We will document this more when the data loading story finalizes. | ||
@@ -383,4 +383,5 @@ React Router Guide | ||
var Routes = Router.Routes; | ||
var NotFoundRoute = Router.NotFoundRoute; | ||
var DefaultRoute = Router.DefaultRoute; | ||
var Link = Router.Link; | ||
``` |
@@ -16,3 +16,7 @@ var LocationActions = require('../actions/LocationActions'); | ||
case LocationActions.POP: | ||
window.scrollTo(position.x, position.y); | ||
if (position) { | ||
window.scrollTo(position.x, position.y); | ||
} else { | ||
window.scrollTo(0, 0); | ||
} | ||
break; | ||
@@ -19,0 +23,0 @@ } |
@@ -15,4 +15,2 @@ var assert = require('assert'); | ||
afterEach(require('../../stores/PathStore').teardown); | ||
describe('A DefaultRoute', function () { | ||
@@ -69,5 +67,5 @@ | ||
var component, rootRoute, defaultRoute; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'none', initialPath: '/users/5' }, | ||
Routes({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: NullHandler }, | ||
@@ -80,3 +78,5 @@ Route({ name: 'home', path: '/users/:id/home', handler: NullHandler }), | ||
) | ||
) | ||
); | ||
component.dispatch('/users/5', done); | ||
}); | ||
@@ -83,0 +83,0 @@ |
@@ -12,4 +12,2 @@ var assert = require('assert'); | ||
afterEach(require('../../stores/PathStore').teardown); | ||
describe('with params and a query', function () { | ||
@@ -23,8 +21,14 @@ var HomeHandler = React.createClass({ | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'none', initialPath: '/mjackson/home' }, | ||
Routes({ location: 'none' }, | ||
Route({ name: 'home', path: '/:username/home', handler: HomeHandler }) | ||
) | ||
); | ||
component.dispatch('/mjackson/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -50,8 +54,14 @@ | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'none', initialPath: '/' }, | ||
Routes({ location: 'none' }, | ||
DefaultRoute({ name: 'home', handler: HomeHandler }) | ||
) | ||
); | ||
component.dispatch('/', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -63,3 +73,3 @@ | ||
it('has its active class name', function () { | ||
it('is active', function () { | ||
var linkComponent = component.getActiveComponent().refs.link; | ||
@@ -70,2 +80,34 @@ expect(linkComponent.getClassName()).toEqual('a-link highlight'); | ||
describe('when the path it links to is active', function () { | ||
var HomeHandler = React.createClass({ | ||
render: function () { | ||
return Link({ ref: 'link', to: '/home', className: 'a-link', activeClassName: 'highlight' }); | ||
} | ||
}); | ||
var component; | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'none' }, | ||
Route({ path: '/home', handler: HomeHandler }) | ||
) | ||
); | ||
component.dispatch('/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
afterEach(function () { | ||
React.unmountComponentAtNode(component.getDOMNode()); | ||
}); | ||
it('is active', function () { | ||
var linkComponent = component.getActiveComponent().refs.link; | ||
expect(linkComponent.getClassName()).toEqual('a-link highlight'); | ||
}); | ||
}); | ||
}); |
@@ -66,5 +66,5 @@ var assert = require('assert'); | ||
var component, rootRoute, notFoundRoute; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'none', initialPath: '/users/5' }, | ||
Routes({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: NullHandler }, | ||
@@ -77,3 +77,5 @@ Route({ name: 'home', path: '/users/:id/home', handler: NullHandler }), | ||
) | ||
) | ||
); | ||
component.dispatch('/users/5', done); | ||
}); | ||
@@ -80,0 +82,0 @@ |
var React = require('react'); | ||
var classSet = require('react/lib/cx'); | ||
var merge = require('react/lib/merge'); | ||
@@ -83,8 +84,11 @@ var ActiveState = require('../mixins/ActiveState'); | ||
getClassName: function () { | ||
var className = this.props.className || ''; | ||
var classNames = {}; | ||
if (this.props.className) | ||
classNames[this.props.className] = true; | ||
if (this.isActive(this.props.to, this.props.params, this.props.query)) | ||
className += ' ' + this.props.activeClassName; | ||
classNames[this.props.activeClassName] = true; | ||
return className; | ||
return classSet(classNames); | ||
}, | ||
@@ -91,0 +95,0 @@ |
@@ -28,6 +28,2 @@ var React = require('react'); | ||
* | ||
* Unlike Ember, a nested route's path does not build upon that of its parents. | ||
* This may seem like it creates more work up front in specifying URLs, but it | ||
* has the nice benefit of decoupling nested UI from "nested" URLs. | ||
* | ||
* The preferred way to configure a router is using JSX. The XML-like syntax is | ||
@@ -34,0 +30,0 @@ * a great way to visualize how routes are laid out in an application. |
var React = require('react'); | ||
var warning = require('react/lib/warning'); | ||
var invariant = require('react/lib/invariant'); | ||
var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; | ||
var copyProperties = require('react/lib/copyProperties'); | ||
var PathStore = require('../stores/PathStore'); | ||
var HashLocation = require('../locations/HashLocation'); | ||
var ActiveContext = require('../mixins/ActiveContext'); | ||
var LocationContext = require('../mixins/LocationContext'); | ||
var RouteContext = require('../mixins/RouteContext'); | ||
var ScrollContext = require('../mixins/ScrollContext'); | ||
var reversedArray = require('../utils/reversedArray'); | ||
@@ -33,3 +35,3 @@ var Transition = require('../utils/Transition'); | ||
var rootParams = getRootMatch(matches).params; | ||
params = route.props.paramNames.reduce(function (params, paramName) { | ||
@@ -76,76 +78,3 @@ params[paramName] = rootParams[paramName]; | ||
function updateMatchComponents(matches, refs) { | ||
var i = 0, component; | ||
while (component = refs.__activeRoute__) { | ||
matches[i++].component = component; | ||
refs = component.refs; | ||
} | ||
} | ||
/** | ||
* Computes the next state for the given component and calls | ||
* callback(error, nextState) when finished. Also runs all | ||
* transition hooks along the way. | ||
*/ | ||
function computeNextState(component, transition, callback) { | ||
if (component.state.path === transition.path) | ||
return callback(); // Nothing to do! | ||
var currentMatches = component.state.matches; | ||
var nextMatches = component.match(transition.path); | ||
warning( | ||
nextMatches, | ||
'No route matches path "' + transition.path + '". Make sure you have ' + | ||
'<Route path="' + transition.path + '"> somewhere in your routes' | ||
); | ||
if (!nextMatches) | ||
nextMatches = []; | ||
var fromMatches, toMatches; | ||
if (currentMatches.length) { | ||
updateMatchComponents(currentMatches, component.refs); | ||
fromMatches = currentMatches.filter(function (match) { | ||
return !hasMatch(nextMatches, match); | ||
}); | ||
toMatches = nextMatches.filter(function (match) { | ||
return !hasMatch(currentMatches, match); | ||
}); | ||
} else { | ||
fromMatches = []; | ||
toMatches = nextMatches; | ||
} | ||
var query = Path.extractQuery(transition.path) || {}; | ||
runTransitionFromHooks(fromMatches, transition, function (error) { | ||
if (error || transition.isAborted) | ||
return callback(error); | ||
runTransitionToHooks(toMatches, transition, query, function (error) { | ||
if (error || transition.isAborted) | ||
return callback(error); | ||
var matches = currentMatches.slice(0, currentMatches.length - fromMatches.length).concat(toMatches); | ||
var rootMatch = getRootMatch(matches); | ||
var params = (rootMatch && rootMatch.params) || {}; | ||
var routes = matches.map(function (match) { | ||
return match.route; | ||
}); | ||
callback(null, { | ||
path: transition.path, | ||
matches: matches, | ||
activeRoutes: routes, | ||
activeParams: params, | ||
activeQuery: query | ||
}); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Calls the willTransitionFrom hook of all handlers in the given matches | ||
@@ -226,2 +155,15 @@ * serially in reverse with the transition object and the current instance of | ||
function updateMatchComponents(matches, refs) { | ||
var match; | ||
for (var i = 0, len = matches.length; i < len; ++i) { | ||
match = matches[i]; | ||
match.component = refs.__activeRoute__; | ||
if (match.component == null) | ||
break; // End of the tree. | ||
refs = match.component.refs; | ||
} | ||
} | ||
function returnNull() { | ||
@@ -231,76 +173,29 @@ return null; | ||
function computeHandlerProps(matches, query) { | ||
var handler = returnNull; | ||
var props = { | ||
ref: null, | ||
params: null, | ||
query: null, | ||
activeRouteHandler: handler, | ||
key: null | ||
}; | ||
reversedArray(matches).forEach(function (match) { | ||
var route = match.route; | ||
props = Route.getUnreservedProps(route.props); | ||
props.ref = '__activeRoute__'; | ||
props.params = match.params; | ||
props.query = query; | ||
props.activeRouteHandler = handler; | ||
// TODO: Can we remove addHandlerKey? | ||
if (route.props.addHandlerKey) | ||
props.key = Path.injectParams(route.props.path, match.params); | ||
handler = function (props, addedProps) { | ||
if (arguments.length > 2 && typeof arguments[2] !== 'undefined') | ||
throw new Error('Passing children to a route handler is not supported'); | ||
return route.props.handler( | ||
copyProperties(props, addedProps) | ||
); | ||
}.bind(this, props); | ||
function routeIsActive(activeRoutes, routeName) { | ||
return activeRoutes.some(function (route) { | ||
return route.props.name === routeName; | ||
}); | ||
return props; | ||
} | ||
var BrowserTransitionHandling = { | ||
function paramsAreActive(activeParams, params) { | ||
for (var property in params) | ||
if (String(activeParams[property]) !== String(params[property])) | ||
return false; | ||
handleTransitionError: function (component, error) { | ||
throw error; // This error probably originated in a transition hook. | ||
}, | ||
return true; | ||
} | ||
handleAbortedTransition: function (component, transition) { | ||
var reason = transition.abortReason; | ||
function queryIsActive(activeQuery, query) { | ||
for (var property in query) | ||
if (String(activeQuery[property]) !== String(query[property])) | ||
return false; | ||
if (reason instanceof Redirect) { | ||
component.replaceWith(reason.to, reason.params, reason.query); | ||
} else { | ||
component.goBack(); | ||
} | ||
} | ||
return true; | ||
} | ||
}; | ||
function defaultTransitionErrorHandler(error) { | ||
// Throw so we don't silently swallow async errors. | ||
throw error; // This error probably originated in a transition hook. | ||
} | ||
var ServerTransitionHandling = { | ||
handleTransitionError: function (component, error) { | ||
// TODO | ||
}, | ||
handleAbortedTransition: function (component, transition) { | ||
// TODO | ||
} | ||
}; | ||
var TransitionHandling = canUseDOM ? BrowserTransitionHandling : ServerTransitionHandling; | ||
var ActiveContext = require('../mixins/ActiveContext'); | ||
var LocationContext = require('../mixins/LocationContext'); | ||
var RouteContext = require('../mixins/RouteContext'); | ||
var ScrollContext = require('../mixins/ScrollContext'); | ||
/** | ||
@@ -316,51 +211,44 @@ * The <Routes> component configures the route hierarchy and renders the | ||
mixins: [ ActiveContext, LocationContext, RouteContext, ScrollContext ], | ||
mixins: [ RouteContext, ActiveContext, LocationContext, ScrollContext ], | ||
propTypes: { | ||
initialPath: React.PropTypes.string, | ||
onChange: React.PropTypes.func | ||
initialMatches: React.PropTypes.array, | ||
onChange: React.PropTypes.func, | ||
onError: React.PropTypes.func.isRequired | ||
}, | ||
getInitialState: function () { | ||
getDefaultProps: function () { | ||
return { | ||
matches: [] | ||
initialPath: null, | ||
initialMatches: [], | ||
onError: defaultTransitionErrorHandler | ||
}; | ||
}, | ||
componentWillMount: function () { | ||
this.handlePathChange(this.props.initialPath); | ||
getInitialState: function () { | ||
return { | ||
path: this.props.initialPath, | ||
matches: this.props.initialMatches | ||
}; | ||
}, | ||
componentDidMount: function () { | ||
PathStore.addChangeListener(this.handlePathChange); | ||
}, | ||
warning( | ||
this._owner == null, | ||
'<Routes> should be rendered directly using React.renderComponent, not ' + | ||
'inside some other component\'s render method' | ||
); | ||
componentWillUnmount: function () { | ||
PathStore.removeChangeListener(this.handlePathChange); | ||
if (this._handleStateChange) { | ||
this._handleStateChange(); | ||
delete this._handleStateChange; | ||
} | ||
}, | ||
handlePathChange: function (_path) { | ||
var path = _path || PathStore.getCurrentPath(); | ||
var actionType = PathStore.getCurrentActionType(); | ||
if (this.state.path === path) | ||
return; // Nothing to do! | ||
if (this.state.path) | ||
this.recordScroll(this.state.path); | ||
var self = this; | ||
this.dispatch(path, function (error, transition) { | ||
if (error) { | ||
TransitionHandling.handleTransitionError(self, error); | ||
} else if (transition.isAborted) { | ||
TransitionHandling.handleAbortedTransition(self, transition); | ||
} else { | ||
self.updateScroll(path, actionType); | ||
if (self.props.onChange) | ||
self.props.onChange.call(self); | ||
} | ||
}); | ||
componentDidUpdate: function () { | ||
if (this._handleStateChange) { | ||
this._handleStateChange(); | ||
delete this._handleStateChange; | ||
} | ||
}, | ||
@@ -386,31 +274,102 @@ | ||
match: function (path) { | ||
return findMatches(Path.withoutQuery(path), this.getRoutes(), this.props.defaultRoute, this.props.notFoundRoute); | ||
var routes = this.getRoutes(); | ||
return findMatches(Path.withoutQuery(path), routes, this.props.defaultRoute, this.props.notFoundRoute); | ||
}, | ||
updateLocation: function (path, actionType) { | ||
if (this.state.path === path) | ||
return; // Nothing to do! | ||
if (this.state.path) | ||
this.recordScroll(this.state.path); | ||
this.dispatch(path, function (error, abortReason, nextState) { | ||
if (error) { | ||
this.props.onError.call(this, error); | ||
} else if (abortReason instanceof Redirect) { | ||
this.replaceWith(abortReason.to, abortReason.params, abortReason.query); | ||
} else if (abortReason) { | ||
this.goBack(); | ||
} else { | ||
this._handleStateChange = this.handleStateChange.bind(this, path, actionType); | ||
this.setState(nextState); | ||
} | ||
}); | ||
}, | ||
handleStateChange: function (path, actionType) { | ||
updateMatchComponents(this.state.matches, this.refs); | ||
this.updateScroll(path, actionType); | ||
if (this.props.onChange) | ||
this.props.onChange.call(this); | ||
}, | ||
/** | ||
* Performs a transition to the given path and calls callback(error, transition) | ||
* with the Transition object when the transition is finished and the component's | ||
* state has been updated accordingly. | ||
* Performs a transition to the given path and calls callback(error, abortReason, nextState) | ||
* when the transition is finished. If there was an error, the first argument will not be null. | ||
* Otherwise, if the transition was aborted for some reason, it will be given in the second arg. | ||
* | ||
* In a transition, the 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 | ||
* In a transition, the 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 transition.wait(promise). | ||
* Both willTransitionFrom and willTransitionTo hooks may either abort or redirect the transition. | ||
* To resolve asynchronously, they may use transition.wait(promise). If no hooks wait, the | ||
* transition will be synchronous. | ||
*/ | ||
dispatch: function (path, callback) { | ||
var transition = new Transition(this, path); | ||
var self = this; | ||
var currentMatches = this.state ? this.state.matches : []; // No state server-side. | ||
var nextMatches = this.match(path) || []; | ||
computeNextState(this, transition, function (error, nextState) { | ||
if (error || nextState == null) | ||
return callback(error, transition); | ||
warning( | ||
nextMatches.length, | ||
'No route matches path "%s". Make sure you have <Route path="%s"> somewhere in your <Routes>', | ||
path, path | ||
); | ||
self.setState(nextState, function () { | ||
callback(null, transition); | ||
var fromMatches, toMatches; | ||
if (currentMatches.length) { | ||
fromMatches = currentMatches.filter(function (match) { | ||
return !hasMatch(nextMatches, match); | ||
}); | ||
toMatches = nextMatches.filter(function (match) { | ||
return !hasMatch(currentMatches, match); | ||
}); | ||
} else { | ||
fromMatches = []; | ||
toMatches = nextMatches; | ||
} | ||
var callbackScope = this; | ||
var query = Path.extractQuery(path) || {}; | ||
runTransitionFromHooks(fromMatches, transition, function (error) { | ||
if (error || transition.isAborted) | ||
return callback.call(callbackScope, error, transition.abortReason); | ||
runTransitionToHooks(toMatches, transition, query, function (error) { | ||
if (error || transition.isAborted) | ||
return callback.call(callbackScope, error, transition.abortReason); | ||
var matches = currentMatches.slice(0, currentMatches.length - fromMatches.length).concat(toMatches); | ||
var rootMatch = getRootMatch(matches); | ||
var params = (rootMatch && rootMatch.params) || {}; | ||
var routes = matches.map(function (match) { | ||
return match.route; | ||
}); | ||
callback.call(callbackScope, null, null, { | ||
path: path, | ||
matches: matches, | ||
activeRoutes: routes, | ||
activeParams: params, | ||
activeQuery: query | ||
}); | ||
}); | ||
}); | ||
@@ -423,3 +382,38 @@ }, | ||
getHandlerProps: function () { | ||
return computeHandlerProps(this.state.matches, this.state.activeQuery); | ||
var matches = this.state.matches; | ||
var query = this.state.activeQuery; | ||
var handler = returnNull; | ||
var props = { | ||
ref: null, | ||
params: null, | ||
query: null, | ||
activeRouteHandler: handler, | ||
key: null | ||
}; | ||
reversedArray(matches).forEach(function (match) { | ||
var route = match.route; | ||
props = Route.getUnreservedProps(route.props); | ||
props.ref = '__activeRoute__'; | ||
props.params = match.params; | ||
props.query = query; | ||
props.activeRouteHandler = handler; | ||
// TODO: Can we remove addHandlerKey? | ||
if (route.props.addHandlerKey) | ||
props.key = Path.injectParams(route.props.path, match.params); | ||
handler = function (props, addedProps) { | ||
if (arguments.length > 2 && typeof arguments[2] !== 'undefined') | ||
throw new Error('Passing children to a route handler is not supported'); | ||
return route.props.handler( | ||
copyProperties(props, addedProps) | ||
); | ||
}.bind(this, props); | ||
}); | ||
return props; | ||
}, | ||
@@ -455,4 +449,4 @@ | ||
route, | ||
'Unable to find a route named "' + to + '". Make sure you have ' + | ||
'a <Route name="' + to + '"> defined somewhere in your <Routes>' | ||
'Unable to find a route named "%s". Make sure you have <Route name="%s"> somewhere in your <Routes>', | ||
to, to | ||
); | ||
@@ -523,2 +517,14 @@ | ||
/** | ||
* Returns true if the given route, params, and query are active. | ||
*/ | ||
isActive: function (to, params, query) { | ||
if (Path.isAbsolute(to)) | ||
return to === this.getCurrentPath(); | ||
return routeIsActive(this.getActiveRoutes(), to) && | ||
paramsAreActive(this.getActiveParams(), params) && | ||
(query == null || queryIsActive(this.getActiveQuery(), query)); | ||
}, | ||
render: function () { | ||
@@ -541,3 +547,4 @@ var match = this.state.matches[0]; | ||
replaceWith: React.PropTypes.func.isRequired, | ||
goBack: React.PropTypes.func.isRequired | ||
goBack: React.PropTypes.func.isRequired, | ||
isActive: React.PropTypes.func.isRequired | ||
}, | ||
@@ -552,3 +559,4 @@ | ||
replaceWith: this.replaceWith, | ||
goBack: this.goBack | ||
goBack: this.goBack, | ||
isActive: this.isActive | ||
}; | ||
@@ -555,0 +563,0 @@ } |
@@ -11,1 +11,4 @@ exports.DefaultRoute = require('./components/DefaultRoute'); | ||
exports.Navigation = require('./mixins/Navigation'); | ||
exports.renderRoutesToString = require('./utils/ServerRendering').renderRoutesToString; | ||
exports.renderRoutesToStaticMarkup = require('./utils/ServerRendering').renderRoutesToStaticMarkup; |
var assert = require('assert'); | ||
var expect = require('expect'); | ||
var React = require('react/addons'); | ||
var ReactTestUtils = React.addons.TestUtils; | ||
var Routes = require('../../components/Routes'); | ||
var Route = require('../../components/Route'); | ||
var ActiveContext = require('../ActiveContext'); | ||
@@ -10,3 +11,2 @@ describe('ActiveContext', function () { | ||
var App = React.createClass({ | ||
mixins: [ ActiveContext ], | ||
render: function () { | ||
@@ -18,17 +18,16 @@ return null; | ||
describe('when a route is active', function () { | ||
var route; | ||
beforeEach(function () { | ||
route = Route({ name: 'products', handler: App }); | ||
}); | ||
describe('and it has no params', function () { | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
App({ | ||
initialActiveState: { | ||
activeRoutes: [ route ] | ||
} | ||
}) | ||
Routes({ location: 'none' }, | ||
Route({ name: 'home', handler: App }) | ||
) | ||
); | ||
component.dispatch('/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -41,3 +40,3 @@ | ||
it('is active', function () { | ||
assert(component.isActive('products')); | ||
assert(component.isActive('home')); | ||
}); | ||
@@ -48,12 +47,14 @@ }); | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
App({ | ||
initialActiveState: { | ||
activeRoutes: [ route ], | ||
activeParams: { id: '123', show: 'true', variant: 456 }, | ||
activeQuery: { search: 'abc', limit: 789 } | ||
} | ||
}) | ||
Routes({ location: 'none' }, | ||
Route({ name: 'products', path: '/products/:id/:variant', handler: App }) | ||
) | ||
); | ||
component.dispatch('/products/123/456?search=abc&limit=789', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -73,3 +74,3 @@ | ||
it('is active', function () { | ||
assert(component.isActive('products', { id: 123 }, { search: 'abc', limit: '789' })); | ||
assert(component.isActive('products', { id: 123 }, { search: 'abc' })); | ||
}); | ||
@@ -80,3 +81,3 @@ }); | ||
it('is not active', function () { | ||
assert(component.isActive('products', { id: 123 }, { search: 'def', limit: '123' }) === false); | ||
assert(component.isActive('products', { id: 123 }, { search: 'def' }) === false); | ||
}); | ||
@@ -88,11 +89,14 @@ }); | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
App({ | ||
initialActiveState: { | ||
activeRoutes: [ route ], | ||
activeParams: { id: 123 } | ||
} | ||
}) | ||
Routes({ location: 'none' }, | ||
Route({ name: 'products', path: '/products/:id', handler: App }) | ||
) | ||
); | ||
component.dispatch('/products/123', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -99,0 +103,0 @@ |
@@ -23,8 +23,14 @@ var assert = require('assert'); | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ initialPath: '/anybody/home' }, | ||
Routes({ location: 'none', onChange: done }, | ||
Route({ name: 'home', path: '/:username/home', handler: NavigationHandler }) | ||
) | ||
); | ||
component.dispatch('/anybody/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -45,8 +51,14 @@ | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ initialPath: '/home' }, | ||
Routes({ location: 'none' }, | ||
Route({ name: 'home', handler: NavigationHandler }) | ||
) | ||
); | ||
component.dispatch('/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -72,8 +84,14 @@ | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'hash', initialPath: '/home' }, | ||
Routes({ location: 'hash' }, | ||
Route({ name: 'home', handler: NavigationHandler }) | ||
) | ||
); | ||
component.dispatch('/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -94,8 +112,14 @@ | ||
var component; | ||
beforeEach(function () { | ||
beforeEach(function (done) { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'history', initialPath: '/home' }, | ||
Routes({ location: 'history' }, | ||
Route({ name: 'home', handler: NavigationHandler }) | ||
) | ||
); | ||
component.dispatch('/home', function (error, abortReason, nextState) { | ||
expect(error).toBe(null); | ||
expect(abortReason).toBe(null); | ||
component.setState(nextState, done); | ||
}); | ||
}); | ||
@@ -102,0 +126,0 @@ |
var React = require('react'); | ||
var copyProperties = require('react/lib/copyProperties'); | ||
function routeIsActive(activeRoutes, routeName) { | ||
return activeRoutes.some(function (route) { | ||
return route.props.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; | ||
} | ||
/** | ||
* A mixin for components that store the active state of routes, URL | ||
* parameters, and query. | ||
* A mixin for components that store the active state of routes, | ||
* URL parameters, and query. | ||
*/ | ||
@@ -33,3 +11,5 @@ var ActiveContext = { | ||
propTypes: { | ||
initialActiveState: React.PropTypes.object | ||
initialActiveRoutes: React.PropTypes.array.isRequired, | ||
initialActiveParams: React.PropTypes.object.isRequired, | ||
initialActiveQuery: React.PropTypes.object.isRequired | ||
}, | ||
@@ -39,3 +19,5 @@ | ||
return { | ||
initialActiveState: {} | ||
initialActiveRoutes: [], | ||
initialActiveParams: {}, | ||
initialActiveQuery: {} | ||
}; | ||
@@ -45,8 +27,6 @@ }, | ||
getInitialState: function () { | ||
var state = this.props.initialActiveState; | ||
return { | ||
activeRoutes: state.activeRoutes || [], | ||
activeParams: state.activeParams || {}, | ||
activeQuery: state.activeQuery || {} | ||
activeRoutes: this.props.initialActiveRoutes, | ||
activeParams: this.props.initialActiveParams, | ||
activeQuery: this.props.initialActiveQuery | ||
}; | ||
@@ -76,21 +56,6 @@ }, | ||
/** | ||
* Returns true if the route with the given name, URL parameters, and | ||
* query are all currently active. | ||
*/ | ||
isActive: function (routeName, params, query) { | ||
var isActive = routeIsActive(this.state.activeRoutes, routeName) && | ||
paramsAreActive(this.state.activeParams, params); | ||
if (query) | ||
return isActive && queryIsActive(this.state.activeQuery, query); | ||
return isActive; | ||
}, | ||
childContextTypes: { | ||
activeRoutes: React.PropTypes.array.isRequired, | ||
activeParams: React.PropTypes.object.isRequired, | ||
activeQuery: React.PropTypes.object.isRequired, | ||
isActive: React.PropTypes.func.isRequired | ||
activeQuery: React.PropTypes.object.isRequired | ||
}, | ||
@@ -102,4 +67,3 @@ | ||
activeParams: this.getActiveParams(), | ||
activeQuery: this.getActiveQuery(), | ||
isActive: this.isActive | ||
activeQuery: this.getActiveQuery() | ||
}; | ||
@@ -106,0 +70,0 @@ } |
@@ -40,18 +40,2 @@ var React = require('react'); | ||
getInitialState: function () { | ||
var location = this.props.location; | ||
if (typeof location === 'string') | ||
location = NAMED_LOCATIONS[location]; | ||
// Automatically fall back to full page refreshes in | ||
// browsers that do not support HTML5 history. | ||
if (location === HistoryLocation && !supportsHistory()) | ||
location = RefreshLocation; | ||
return { | ||
location: location | ||
}; | ||
}, | ||
componentWillMount: function () { | ||
@@ -65,6 +49,21 @@ var location = this.getLocation(); | ||
if (location) | ||
if (location) { | ||
PathStore.setup(location); | ||
PathStore.addChangeListener(this.handlePathChange); | ||
if (this.updateLocation) | ||
this.updateLocation(PathStore.getCurrentPath(), PathStore.getCurrentActionType()); | ||
} | ||
}, | ||
componentWillUnmount: function () { | ||
if (this.getLocation()) | ||
PathStore.removeChangeListener(this.handlePathChange); | ||
}, | ||
handlePathChange: function () { | ||
if (this.updateLocation) | ||
this.updateLocation(PathStore.getCurrentPath(), PathStore.getCurrentActionType()); | ||
}, | ||
/** | ||
@@ -74,3 +73,17 @@ * Returns the location object this component uses. | ||
getLocation: function () { | ||
return this.state.location; | ||
if (this._location == null) { | ||
var location = this.props.location; | ||
if (typeof location === 'string') | ||
location = NAMED_LOCATIONS[location]; | ||
// Automatically fall back to full page refreshes in | ||
// browsers that do not support HTML5 history. | ||
if (location === HistoryLocation && !supportsHistory()) | ||
location = RefreshLocation; | ||
this._location = location; | ||
} | ||
return this._location; | ||
}, | ||
@@ -77,0 +90,0 @@ |
@@ -122,9 +122,5 @@ var React = require('react'); | ||
getInitialState: function () { | ||
var namedRoutes = {}; | ||
return { | ||
routes: processRoutes(this.props.children, this, namedRoutes), | ||
namedRoutes: namedRoutes | ||
}; | ||
_processRoutes: function () { | ||
this._namedRoutes = {}; | ||
this._routes = processRoutes(this.props.children, this, this._namedRoutes); | ||
}, | ||
@@ -136,3 +132,6 @@ | ||
getRoutes: function () { | ||
return this.state.routes; | ||
if (this._routes == null) | ||
this._processRoutes(); | ||
return this._routes; | ||
}, | ||
@@ -144,3 +143,6 @@ | ||
getNamedRoutes: function () { | ||
return this.state.namedRoutes; | ||
if (this._namedRoutes == null) | ||
this._processRoutes(); | ||
return this._namedRoutes; | ||
}, | ||
@@ -152,3 +154,4 @@ | ||
getRouteByName: function (routeName) { | ||
return this.state.namedRoutes[routeName] || null; | ||
var namedRoutes = this.getNamedRoutes(); | ||
return namedRoutes[routeName] || null; | ||
}, | ||
@@ -155,0 +158,0 @@ |
@@ -49,28 +49,12 @@ var React = require('react'); | ||
getInitialState: function () { | ||
var behavior = this.props.scrollBehavior; | ||
if (typeof behavior === 'string') | ||
behavior = NAMED_SCROLL_BEHAVIORS[behavior]; | ||
return { | ||
scrollBehavior: behavior | ||
}; | ||
}, | ||
componentWillMount: function () { | ||
var behavior = this.getScrollBehavior(); | ||
invariant( | ||
behavior == null || canUseDOM, | ||
this.getScrollBehavior() == null || canUseDOM, | ||
'Cannot use scroll behavior without a DOM' | ||
); | ||
if (behavior != null) | ||
this._scrollPositions = {}; | ||
}, | ||
recordScroll: function (path) { | ||
if (this._scrollPositions) | ||
this._scrollPositions[path] = getWindowScrollPosition(); | ||
var positions = this.getScrollPositions(); | ||
positions[path] = getWindowScrollPosition(); | ||
}, | ||
@@ -80,6 +64,6 @@ | ||
var behavior = this.getScrollBehavior(); | ||
var position = this.getScrollPosition(path) || null; | ||
if (behavior != null && this._scrollPositions.hasOwnProperty(path)) { | ||
behavior.updateScrollPosition(this._scrollPositions[path], actionType); | ||
} | ||
if (behavior) | ||
behavior.updateScrollPosition(position, actionType); | ||
}, | ||
@@ -91,5 +75,32 @@ | ||
getScrollBehavior: function () { | ||
return this.state.scrollBehavior; | ||
if (this._scrollBehavior == null) { | ||
var behavior = this.props.scrollBehavior; | ||
if (typeof behavior === 'string') | ||
behavior = NAMED_SCROLL_BEHAVIORS[behavior]; | ||
this._scrollBehavior = behavior; | ||
} | ||
return this._scrollBehavior; | ||
}, | ||
/** | ||
* Returns a hash of URL paths to their last known scroll positions. | ||
*/ | ||
getScrollPositions: function () { | ||
if (this._scrollPositions == null) | ||
this._scrollPositions = {}; | ||
return this._scrollPositions; | ||
}, | ||
/** | ||
* Returns the last known scroll position for the given URL path. | ||
*/ | ||
getScrollPosition: function (path) { | ||
var positions = this.getScrollPositions(); | ||
return positions[path]; | ||
}, | ||
childContextTypes: { | ||
@@ -96,0 +107,0 @@ scrollBehavior: React.PropTypes.object // Not required on the server. |
@@ -50,2 +50,30 @@ var expect = require('expect'); | ||
describe('and the pattern is optional', function () { | ||
var pattern = 'comments/:id?/edit' | ||
describe('and the path matches with supplied param', function () { | ||
it('returns an object with the params', function () { | ||
expect(Path.extractParams(pattern, 'comments/123/edit')).toEqual({ id: '123' }); | ||
}); | ||
}); | ||
describe('and the path matches without supplied param', function () { | ||
it('returns an object with param set to null', function () { | ||
expect(Path.extractParams(pattern, 'comments//edit')).toEqual({id: null}); | ||
}); | ||
}); | ||
}); | ||
describe('and the pattern and forward slash are optional', function () { | ||
var pattern = 'comments/:id?/?edit' | ||
describe('and the path matches with supplied param', function () { | ||
it('returns an object with the params', function () { | ||
expect(Path.extractParams(pattern, 'comments/123/edit')).toEqual({ id: '123' }); | ||
}); | ||
}); | ||
describe('and the path matches without supplied param', function () { | ||
it('returns an object with param set to null', function () { | ||
expect(Path.extractParams(pattern, 'comments/edit')).toEqual({id: null}); | ||
}); | ||
}); | ||
}); | ||
describe('and the path does not match', function () { | ||
@@ -170,2 +198,26 @@ it('returns null', function () { | ||
describe('and a param is optional', function () { | ||
var pattern = 'comments/:id?/edit'; | ||
it('returns the correct path when param is supplied', function () { | ||
expect(Path.injectParams(pattern, {id:'123'})).toEqual('comments/123/edit'); | ||
}); | ||
it('returns the correct path when param is not supplied', function () { | ||
expect(Path.injectParams(pattern, {})).toEqual('comments//edit'); | ||
}); | ||
}); | ||
describe('and a param and forward slash are optional', function () { | ||
var pattern = 'comments/:id?/?edit'; | ||
it('returns the correct path when param is supplied', function () { | ||
expect(Path.injectParams(pattern, {id:'123'})).toEqual('comments/123/edit'); | ||
}); | ||
it('returns the correct path when param is not supplied', function () { | ||
expect(Path.injectParams(pattern, {})).toEqual('comments/edit'); | ||
}); | ||
}); | ||
describe('and all params are present', function () { | ||
@@ -172,0 +224,0 @@ it('returns the correct path', function () { |
@@ -18,3 +18,4 @@ var invariant = require('react/lib/invariant'); | ||
var paramCompileMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|[*.()\[\]\\+|{}^$]/g; | ||
var paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|[*]/g; | ||
var paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$?]*[?]?)|[*]/g; | ||
var paramInjectTrailingSlashMatcher = /\/\/\?|\/\?/g; | ||
var queryMatcher = /\?(.+)/; | ||
@@ -90,6 +91,14 @@ | ||
invariant( | ||
params[paramName] != null, | ||
'Missing "' + paramName + '" parameter for path "' + pattern + '"' | ||
); | ||
// If param is optional don't check for existence | ||
if (paramName.slice(-1) !== '?') { | ||
invariant( | ||
params[paramName] != null, | ||
'Missing "' + paramName + '" parameter for path "' + pattern + '"' | ||
); | ||
} else { | ||
paramName = paramName.slice(0, -1) | ||
if (params[paramName] == null) { | ||
return ''; | ||
} | ||
} | ||
@@ -109,3 +118,3 @@ var segment; | ||
return encodeURLPath(segment); | ||
}); | ||
}).replace(paramInjectTrailingSlashMatcher, '/'); | ||
}, | ||
@@ -112,0 +121,0 @@ |
{ | ||
"name": "react-router", | ||
"version": "0.9.3", | ||
"version": "0.9.4", | ||
"description": "A complete routing library for React.js", | ||
@@ -26,4 +26,6 @@ "main": "./modules/index", | ||
"browserify-shim": "3.6.0", | ||
"bundle-loader": "0.5.1", | ||
"envify": "1.2.0", | ||
"expect": "0.1.1", | ||
"jsx-loader": "0.11.2", | ||
"karma": "0.12.16", | ||
@@ -39,3 +41,5 @@ "karma-browserify": "^0.2.1", | ||
"rf-release": "0.3.2", | ||
"uglify-js": "2.4.15" | ||
"uglify-js": "2.4.15", | ||
"webpack": "1.4.5", | ||
"webpack-dev-server": "1.6.5" | ||
}, | ||
@@ -42,0 +46,0 @@ "peerDependencies": { |
@@ -14,2 +14,4 @@ React Router | ||
[Try it out on JSBin](http://jsbin.com/sixose/1/edit) | ||
Important Notes | ||
@@ -51,3 +53,3 @@ --------------- | ||
There is also a UMD build available on bower, find the library on | ||
There is also a global build available on bower, find the library on | ||
`window.ReactRouter`. | ||
@@ -60,3 +62,3 @@ | ||
- Modular construction of route hierarchy | ||
- Fully asynchronous transition hooks | ||
- Sync and async transition hooks | ||
- Transition abort / redirect / retry | ||
@@ -67,3 +69,6 @@ - Dynamic segments | ||
- Multiple root routes | ||
- Hash or HTML5 history URLs | ||
- Hash or HTML5 history (with fallback) URLs | ||
- Declarative Redirect routes | ||
- Declarative NotFound routes | ||
- Browser scroll behavior with transitions | ||
@@ -70,0 +75,0 @@ Check out the `examples` directory to see how simple previously complex UI |
@@ -16,2 +16,3 @@ require('./modules/components/__tests__/DefaultRoute-test'); | ||
require('./modules/utils/__tests__/Path-test'); | ||
require('./modules/utils/__tests__/ServerRendering-test'); | ||
@@ -18,0 +19,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
493395
70
10313
143
19