react-router
Advanced tools
Comparing version 0.8.0 to 0.9.0
@@ -0,22 +1,28 @@ | ||
v0.9.0 - Mon, 06 Oct 2014 19:37:27 GMT | ||
-------------------------------------- | ||
- [5aae2a8](../../commit/5aae2a8) [added] onChange event to Routes | ||
- [ba65269](../../commit/ba65269) [removed] AsyncState | ||
v0.8.0 - Sat, 04 Oct 2014 05:39:02 GMT | ||
-------------------------------------- | ||
- [4d8026f](../../commit/4d8026f) [fixed] Corrected CONTRIBUTING.md by replacing 'script' path with 'scripts' | ||
- [d2aa7cb](../../commit/d2aa7cb) [added] <Routes location="none"> | ||
- [637c0ac](../../commit/637c0ac) [added] <Routes fixedPath> | ||
- [d2aa7cb](../../commit/d2aa7cb) [added] `<Routes location="none">` | ||
- [637c0ac](../../commit/637c0ac) [added] `<Routes fixedPath>` | ||
- [f2bf4bd](../../commit/f2bf4bd) [removed] RouteStore | ||
- [47f0599](../../commit/47f0599) [changed] Remove preserveScrollPosition, add scrollStrategy | ||
- [2f014b7](../../commit/2f014b7) [fixed] Document the name prop passed to RouteHandlers | ||
- [f2bf4bd](../../commit/f2bf4bd) [added] Router.PathState for keeping track of the current URL path | ||
- [f2bf4bd](../../commit/f2bf4bd) [added] Router.RouteLookup for looking up routes | ||
- [f2bf4bd](../../commit/f2bf4bd) [added] Router.Transitions for transitioning to other routes | ||
- [f2bf4bd](../../commit/f2bf4bd) [added] Pluggable scroll behaviors | ||
- [f2bf4bd](../../commit/f2bf4bd) [changed] `<Routes preserveScrollPosition>` => `<Routes scrollBehavior>` | ||
- [f2bf4bd](../../commit/f2bf4bd) [removed] `<Route preserveScrollPosition>` | ||
- [f2bf4bd](../../commit/f2bf4bd) [removed] Router.transitionTo, Router.replaceWith, Router.goBack | ||
- [97dbf2d](../../commit/97dbf2d) [added] transition.wait(promise) | ||
- [cc9f145](../../commit/cc9f145) [changed] Give path listeners a chance to update state before mounting | ||
- [6af24bd](../../commit/6af24bd) [changed] Give ActiveState a chance to update state before mounting | ||
- [3787179](../../commit/3787179) [changed] Transition retry now uses replaceWith. | ||
- [1b16b56](../../commit/1b16b56) [fixed] syntax error in documentation overview | ||
- [e0b708f](../../commit/e0b708f) [added] Ability to transitionTo absolute URLs | ||
- [c1493b5](../../commit/c1493b5) [changed] #259 support dots in named params | ||
- [4849166](../../commit/4849166) [changed] Renamed Routes#dispatch => Routes#transitionTo | ||
- [c373d10](../../commit/c373d10) [changed] Only replaceWith/goBack when DOM is available | ||
- [a4ce7c8](../../commit/a4ce7c8) [changed] isActive is an instance method [removed] <Routes onActiveStateChange> | ||
- [a4ce7c8](../../commit/a4ce7c8) [changed] isActive is an instance method | ||
- [a4ce7c8](../../commit/a4ce7c8) [removed] `<Routes onActiveStateChange>` | ||
v0.7.0 - Tue, 02 Sep 2014 16:42:28 GMT | ||
@@ -23,0 +29,0 @@ -------------------------------------- |
@@ -42,6 +42,2 @@ API: `Routes` (component) | ||
### `fixedPath` | ||
TODO | ||
### `onAbortedTransition` | ||
@@ -48,0 +44,0 @@ |
@@ -10,7 +10,5 @@ API: `Location` (object) | ||
### `setup(onChange)` | ||
### `setup()` | ||
Called when the router is first setup. Whenever an external actor should | ||
cause the router to react, call `onChange` (for example, on | ||
`window.hashchange`). | ||
Called when the router is first setup. | ||
@@ -34,6 +32,2 @@ ### `teardown` | ||
### `getCurrentPath` | ||
Should return the current path as a string. | ||
### `toString` | ||
@@ -52,3 +46,3 @@ | ||
setup: function (onChange) {}, | ||
setup: function () {}, | ||
@@ -63,4 +57,2 @@ teardown: function () {}, | ||
getCurrentPath: function () {}, | ||
toString: function () {} | ||
@@ -70,2 +62,1 @@ | ||
``` | ||
@@ -10,2 +10,14 @@ API: `ActiveState` (mixin) | ||
### `getActiveRoutes()` | ||
Returns an array of the `<Route>`s that are currently active. | ||
### `getActiveParams()` | ||
Returns a hash of the currently active URL params. | ||
### `getActiveQuery()` | ||
Returns a hash of the currently active query params. | ||
### `isActive(routeName, params, query)` | ||
@@ -16,9 +28,2 @@ | ||
Lifecycle Methods | ||
----------------- | ||
### `updateActiveState` | ||
Called when the active state changes. | ||
Example | ||
@@ -31,4 +36,4 @@ ------- | ||
```js | ||
var Link = require('react-router/Link'); | ||
var ActiveState = require('react-router/ActiveState'); | ||
var Link = require('react-router').Link; | ||
var ActiveState = require('react-router').ActiveState; | ||
@@ -39,14 +44,5 @@ var Tab = React.createClass({ | ||
getInitialState: function () { | ||
return { isActive: false }; | ||
}, | ||
updateActiveState: function () { | ||
this.setState({ | ||
isActive: this.isActive(this.props.to, this.props.params, this.props.query) | ||
}) | ||
}, | ||
render: function() { | ||
var className = this.state.isActive ? 'active' : ''; | ||
var isActive = this.isActive(this.props.to, this.props.params, this.props.query); | ||
var className = isActive ? 'active' : ''; | ||
var link = Link(this.props); | ||
@@ -62,2 +58,1 @@ return <li className={className}>{link}</li>; | ||
``` | ||
@@ -15,9 +15,10 @@ React Router API | ||
- Misc | ||
- [`transition`](/docs/api/misc/transition.md) | ||
- Mixins | ||
- [`ActiveState`](/docs/api/mixins/ActiveState.md) | ||
- [`AsyncState`](/docs/api/mixins/AsyncState.md) | ||
- [`CurrentPath`](/docs/api/mixins/CurrentPath.md) | ||
- [`Navigation`](/docs/api/mixins/Navigation.md) | ||
- Misc | ||
- [`transition`](/docs/api/misc/transition.md) | ||
@@ -27,8 +28,10 @@ Public Modules | ||
All modules found at the repository root are considered public. You can | ||
require them conveniently with `var Route = require('react-router/Route');` etc. | ||
While there are many modules in this repository, only those found on the | ||
default export are considered public. | ||
Note that we do not support requiring modules from our `modules` | ||
directory. (No notes in the changelog, no changes to the versioning of | ||
the lib, etc.) | ||
```js | ||
var Router = require('react-router'); | ||
var Link = Router.Link // yes | ||
var Link = require('react-router/modules/components/Link') // no | ||
``` | ||
@@ -307,4 +307,4 @@ React Router Guide | ||
their scroll position. If they visit a new route, it will automatically | ||
scroll the window to the top. You can opt out of this with the | ||
`preserverScrollPosition` option on [Routes][Routes] or [Route][Route]. | ||
scroll the window to the top. You can configure this options on | ||
[Routes][Routes]. | ||
@@ -375,1 +375,13 @@ Bells and Whistles | ||
CommonJS Guide | ||
-------------- | ||
In order for the above examples to work in a CommonJS environment you'll need to `require` the following: | ||
``` | ||
var Router = require('react-router'); | ||
var Route = Router.Route; | ||
var Routes = Router.Routes; | ||
var DefaultRoute = Router.DefaultRoute; | ||
var Link = Router.Link; | ||
``` |
@@ -5,14 +5,14 @@ var assert = require('assert'); | ||
var ReactTestUtils = React.addons.TestUtils; | ||
var RouteContainer = require('../../mixins/RouteContainer'); | ||
var TransitionHandler = require('../../mixins/TransitionHandler'); | ||
var PathStore = require('../../stores/PathStore'); | ||
var DefaultRoute = require('../DefaultRoute'); | ||
var Routes = require('../Routes'); | ||
var Route = require('../Route'); | ||
var DefaultRoute = require('../DefaultRoute'); | ||
afterEach(function () { | ||
// For some reason unmountComponentAtNode doesn't call componentWillUnmount :/ | ||
PathStore.removeAllChangeListeners(); | ||
var NullHandler = React.createClass({ | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
describe('A DefaultRoute', function () { | ||
it('has a null path', function () { | ||
@@ -22,9 +22,2 @@ expect(DefaultRoute({ path: '/' }).props.path).toBe(null); | ||
var App = React.createClass({ | ||
mixins: [ RouteContainer ], | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
describe('at the root of a container', function () { | ||
@@ -34,4 +27,4 @@ var component, route; | ||
component = ReactTestUtils.renderIntoDocument( | ||
App(null, | ||
route = DefaultRoute({ handler: App }) | ||
Routes({ location: 'none' }, | ||
route = DefaultRoute({ handler: NullHandler }) | ||
) | ||
@@ -54,5 +47,5 @@ ); | ||
component = ReactTestUtils.renderIntoDocument( | ||
App(null, | ||
route = Route({ handler: App }, | ||
defaultRoute = DefaultRoute({ handler: App }) | ||
Routes({ location: 'none' }, | ||
route = Route({ handler: NullHandler }, | ||
defaultRoute = DefaultRoute({ handler: NullHandler }) | ||
) | ||
@@ -71,11 +64,6 @@ ) | ||
}); | ||
}); | ||
describe('when no child routes match a URL, but the parent\'s path matches', function () { | ||
var App = React.createClass({ | ||
mixins: [ TransitionHandler ], | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
@@ -85,8 +73,8 @@ var component, rootRoute, defaultRoute; | ||
component = ReactTestUtils.renderIntoDocument( | ||
App({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: App }, | ||
Route({ name: 'home', path: '/users/:id/home', handler: App }), | ||
Routes({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: NullHandler }, | ||
Route({ name: 'home', path: '/users/:id/home', handler: NullHandler }), | ||
// Make it the middle sibling to test order independence. | ||
defaultRoute = DefaultRoute({ handler: App }), | ||
Route({ name: 'news', path: '/users/:id/news', handler: App }) | ||
defaultRoute = DefaultRoute({ handler: NullHandler }), | ||
Route({ name: 'news', path: '/users/:id/news', handler: NullHandler }) | ||
) | ||
@@ -108,2 +96,3 @@ ) | ||
}); | ||
}); |
@@ -5,10 +5,37 @@ var assert = require('assert'); | ||
var ReactTestUtils = React.addons.TestUtils; | ||
var PathStore = require('../../stores/PathStore'); | ||
var DefaultRoute = require('../DefaultRoute'); | ||
var Routes = require('../Routes'); | ||
var Route = require('../Route'); | ||
var Link = require('../Link'); | ||
describe('A Link', function () { | ||
describe('with params and a query', function () { | ||
var HomeHandler = React.createClass({ | ||
render: function () { | ||
return Link({ ref: 'link', to: 'home', params: { username: 'mjackson' }, query: { awesome: true } }); | ||
} | ||
}); | ||
var component; | ||
beforeEach(function () { | ||
component = ReactTestUtils.renderIntoDocument( | ||
Routes({ location: 'history', initialPath: '/mjackson/home' }, | ||
Route({ name: 'home', path: '/:username/home', handler: HomeHandler }) | ||
) | ||
); | ||
}); | ||
afterEach(function () { | ||
React.unmountComponentAtNode(component.getDOMNode()); | ||
}); | ||
it('knows how to make its href', function () { | ||
var linkComponent = component.getActiveComponent().refs.link; | ||
expect(linkComponent.getHref()).toEqual('/mjackson/home?awesome=true'); | ||
}); | ||
}); | ||
describe('when its route is active', function () { | ||
var Home = React.createClass({ | ||
var HomeHandler = React.createClass({ | ||
render: function () { | ||
@@ -23,3 +50,3 @@ return Link({ ref: 'link', to: 'home', className: 'a-link', activeClassName: 'highlight' }); | ||
Routes(null, | ||
DefaultRoute({ name: 'home', handler: Home }) | ||
DefaultRoute({ name: 'home', handler: HomeHandler }) | ||
) | ||
@@ -31,16 +58,10 @@ ); | ||
React.unmountComponentAtNode(component.getDOMNode()); | ||
// For some reason unmountComponentAtNode doesn't call componentWillUnmount :/ | ||
PathStore.removeAllChangeListeners(); | ||
}); | ||
it('is active', function () { | ||
var linkComponent = component.getActiveRoute().refs.link; | ||
assert(linkComponent.isActive); | ||
}); | ||
it('has its active class name', function () { | ||
var linkComponent = component.getActiveRoute().refs.link; | ||
var linkComponent = component.getActiveComponent().refs.link; | ||
expect(linkComponent.getClassName()).toEqual('a-link highlight'); | ||
}); | ||
}); | ||
}); |
@@ -5,14 +5,14 @@ var assert = require('assert'); | ||
var ReactTestUtils = React.addons.TestUtils; | ||
var RouteContainer = require('../../mixins/RouteContainer'); | ||
var TransitionHandler = require('../../mixins/TransitionHandler'); | ||
var PathStore = require('../../stores/PathStore'); | ||
var NotFoundRoute = require('../NotFoundRoute'); | ||
var Routes = require('../Routes'); | ||
var Route = require('../Route'); | ||
var NotFoundRoute = require('../NotFoundRoute'); | ||
afterEach(function () { | ||
// For some reason unmountComponentAtNode doesn't call componentWillUnmount :/ | ||
PathStore.removeAllChangeListeners(); | ||
var NullHandler = React.createClass({ | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
describe('A NotFoundRoute', function () { | ||
it('has a null path', function () { | ||
@@ -22,9 +22,2 @@ expect(NotFoundRoute({ path: '/' }).props.path).toBe(null); | ||
var App = React.createClass({ | ||
mixins: [ RouteContainer ], | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
describe('at the root of a container', function () { | ||
@@ -34,4 +27,4 @@ var component, route; | ||
component = ReactTestUtils.renderIntoDocument( | ||
App(null, | ||
route = NotFoundRoute({ handler: App }) | ||
Routes({ location: 'none' }, | ||
route = NotFoundRoute({ handler: NullHandler }) | ||
) | ||
@@ -54,5 +47,5 @@ ); | ||
component = ReactTestUtils.renderIntoDocument( | ||
App(null, | ||
route = Route({ handler: App }, | ||
notFoundRoute = NotFoundRoute({ handler: App }) | ||
Routes({ location: 'none' }, | ||
route = Route({ handler: NullHandler }, | ||
notFoundRoute = NotFoundRoute({ handler: NullHandler }) | ||
) | ||
@@ -71,11 +64,6 @@ ) | ||
}); | ||
}); | ||
describe('when no child routes match a URL, but the beginning of the parent\'s path matches', function () { | ||
var App = React.createClass({ | ||
mixins: [ TransitionHandler ], | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
@@ -85,8 +73,8 @@ var component, rootRoute, notFoundRoute; | ||
component = ReactTestUtils.renderIntoDocument( | ||
App({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: App }, | ||
Route({ name: 'home', path: '/users/:id/home', handler: App }), | ||
Routes({ location: 'none' }, | ||
rootRoute = Route({ name: 'user', path: '/users/:id', handler: NullHandler }, | ||
Route({ name: 'home', path: '/users/:id/home', handler: NullHandler }), | ||
// Make it the middle sibling to test order independence. | ||
notFoundRoute = NotFoundRoute({ handler: App }), | ||
Route({ name: 'news', path: '/users/:id/news', handler: App }) | ||
notFoundRoute = NotFoundRoute({ handler: NullHandler }), | ||
Route({ name: 'news', path: '/users/:id/news', handler: NullHandler }) | ||
) | ||
@@ -108,2 +96,3 @@ ) | ||
}); | ||
}); |
@@ -5,3 +5,2 @@ var assert = require('assert'); | ||
var ReactTestUtils = React.addons.TestUtils; | ||
var PathStore = require('../../stores/PathStore'); | ||
var Routes = require('../Routes'); | ||
@@ -14,5 +13,6 @@ var Route = require('../Route'); | ||
afterEach(function () { | ||
// For some reason unmountComponentAtNode doesn't call componentWillUnmount :/ | ||
PathStore.removeAllChangeListeners(); | ||
var NullHandler = React.createClass({ | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
@@ -22,8 +22,2 @@ | ||
var App = React.createClass({ | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
describe('that matches a URL', function () { | ||
@@ -34,4 +28,4 @@ var component; | ||
Routes(null, | ||
Route({ handler: App }, | ||
Route({ path: '/a/b/c', handler: App }) | ||
Route({ handler: NullHandler }, | ||
Route({ path: '/a/b/c', handler: NullHandler }) | ||
) | ||
@@ -61,4 +55,4 @@ ) | ||
Routes(null, | ||
Route({ handler: App }, | ||
Route({ path: '/posts/:id/edit', handler: App }) | ||
Route({ handler: NullHandler }, | ||
Route({ path: '/posts/:id/edit', handler: NullHandler }) | ||
) | ||
@@ -83,56 +77,2 @@ ) | ||
describe('when a transition is aborted', function () { | ||
it('triggers onAbortedTransition', function (done) { | ||
var App = React.createClass({ | ||
statics: { | ||
willTransitionTo: function (transition) { | ||
transition.abort(); | ||
} | ||
}, | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
function handleAbortedTransition(transition) { | ||
assert(transition); | ||
done(); | ||
} | ||
ReactTestUtils.renderIntoDocument( | ||
Routes({ onAbortedTransition: handleAbortedTransition }, | ||
Route({ handler: App }) | ||
) | ||
); | ||
}); | ||
}); | ||
describe('when there is an error in a transition hook', function () { | ||
it('triggers onTransitionError', function (done) { | ||
var App = React.createClass({ | ||
statics: { | ||
willTransitionTo: function (transition) { | ||
throw new Error('boom!'); | ||
} | ||
}, | ||
render: function () { | ||
return React.DOM.div(); | ||
} | ||
}); | ||
function handleTransitionError(error) { | ||
assert(error); | ||
expect(error.message).toEqual('boom!'); | ||
done(); | ||
} | ||
ReactTestUtils.renderIntoDocument( | ||
Routes({ onTransitionError: handleTransitionError }, | ||
Route({ handler: App }) | ||
) | ||
); | ||
}); | ||
}); | ||
}); |
var React = require('react'); | ||
var merge = require('react/lib/merge'); | ||
var ActiveState = require('../mixins/ActiveState'); | ||
var Transitions = require('../mixins/Transitions'); | ||
var Navigation = require('../mixins/Navigation'); | ||
@@ -36,7 +36,7 @@ function isLeftClickEvent(event) { | ||
mixins: [ ActiveState, Transitions ], | ||
mixins: [ ActiveState, Navigation ], | ||
propTypes: { | ||
activeClassName: React.PropTypes.string.isRequired, | ||
to: React.PropTypes.string.isRequired, | ||
activeClassName: React.PropTypes.string.isRequired, | ||
params: React.PropTypes.object, | ||
@@ -53,26 +53,8 @@ query: React.PropTypes.object, | ||
getInitialState: function () { | ||
return { | ||
isActive: false | ||
}; | ||
}, | ||
updateActiveState: function () { | ||
this.setState({ | ||
isActive: this.isActive(this.props.to, this.props.params, this.props.query) | ||
}); | ||
}, | ||
componentWillReceiveProps: function (nextProps) { | ||
this.setState({ | ||
isActive: this.isActive(nextProps.to, nextProps.params, nextProps.query) | ||
}); | ||
}, | ||
handleClick: function (event) { | ||
var allowTransition = true; | ||
var onClickResult; | ||
var clickResult; | ||
if (this.props.onClick) | ||
onClickResult = this.props.onClick(event); | ||
clickResult = this.props.onClick(event); | ||
@@ -82,3 +64,3 @@ if (isModifiedEvent(event) || !isLeftClickEvent(event)) | ||
if (onClickResult === false || event.defaultPrevented === true) | ||
if (clickResult === false || event.defaultPrevented === true) | ||
allowTransition = false; | ||
@@ -106,3 +88,3 @@ | ||
if (this.state.isActive) | ||
if (this.isActive(this.props.to, this.props.params, this.props.query)) | ||
className += ' ' + this.props.activeClassName; | ||
@@ -109,0 +91,0 @@ |
var React = require('react'); | ||
var TransitionHandler = require('../mixins/TransitionHandler'); | ||
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 reversedArray = require('../utils/reversedArray'); | ||
var Transition = require('../utils/Transition'); | ||
var Redirect = require('../utils/Redirect'); | ||
var Path = require('../utils/Path'); | ||
var Route = require('./Route'); | ||
function makeMatch(route, params) { | ||
return { route: route, params: params }; | ||
} | ||
function getRootMatch(matches) { | ||
return matches[matches.length - 1]; | ||
} | ||
function findMatches(path, routes, defaultRoute, notFoundRoute) { | ||
var matches = null, route, params; | ||
for (var i = 0, len = routes.length; i < len; ++i) { | ||
route = routes[i]; | ||
// Check the subtree first to find the most deeply-nested match. | ||
matches = findMatches(path, route.props.children, route.props.defaultRoute, route.props.notFoundRoute); | ||
if (matches != null) { | ||
var rootParams = getRootMatch(matches).params; | ||
params = route.props.paramNames.reduce(function (params, paramName) { | ||
params[paramName] = rootParams[paramName]; | ||
return params; | ||
}, {}); | ||
matches.unshift(makeMatch(route, params)); | ||
return matches; | ||
} | ||
// No routes in the subtree matched, so check this route. | ||
params = Path.extractParams(route.props.path, path); | ||
if (params) | ||
return [ makeMatch(route, params) ]; | ||
} | ||
// No routes matched, so try the default route if there is one. | ||
if (defaultRoute && (params = Path.extractParams(defaultRoute.props.path, path))) | ||
return [ makeMatch(defaultRoute, params) ]; | ||
// Last attempt: does the "not found" route match? | ||
if (notFoundRoute && (params = Path.extractParams(notFoundRoute.props.path, path))) | ||
return [ makeMatch(notFoundRoute, params) ]; | ||
return matches; | ||
} | ||
function hasMatch(matches, match) { | ||
return matches.some(function (m) { | ||
if (m.route !== match.route) | ||
return false; | ||
for (var property in m.params) | ||
if (m.params[property] !== match.params[property]) | ||
return false; | ||
return true; | ||
}); | ||
} | ||
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 | ||
* serially in reverse with the transition object and the current instance of | ||
* the route's handler, so that the deepest nested handlers are called first. | ||
* Calls callback(error) when finished. | ||
*/ | ||
function runTransitionFromHooks(matches, transition, callback) { | ||
var hooks = reversedArray(matches).map(function (match) { | ||
return function () { | ||
var handler = match.route.props.handler; | ||
if (!transition.isAborted && handler.willTransitionFrom) | ||
return handler.willTransitionFrom(transition, match.component); | ||
var promise = transition.promise; | ||
delete transition.promise; | ||
return promise; | ||
}; | ||
}); | ||
runHooks(hooks, callback); | ||
} | ||
/** | ||
* Calls the willTransitionTo hook of all handlers in the given matches | ||
* serially with the transition object and any params that apply to that | ||
* handler. Calls callback(error) when finished. | ||
*/ | ||
function runTransitionToHooks(matches, transition, query, callback) { | ||
var hooks = matches.map(function (match) { | ||
return function () { | ||
var handler = match.route.props.handler; | ||
if (!transition.isAborted && handler.willTransitionTo) | ||
handler.willTransitionTo(transition, match.params, query); | ||
var promise = transition.promise; | ||
delete transition.promise; | ||
return promise; | ||
}; | ||
}); | ||
runHooks(hooks, callback); | ||
} | ||
/** | ||
* Runs all hook functions serially and calls callback(error) when finished. | ||
* A hook may return a promise if it needs to execute asynchronously. | ||
*/ | ||
function runHooks(hooks, callback) { | ||
try { | ||
var promise = hooks.reduce(function (promise, hook) { | ||
// The first hook to use transition.wait makes the rest | ||
// of the transition async from that point forward. | ||
return promise ? promise.then(hook) : hook(); | ||
}, null); | ||
} catch (error) { | ||
return callback(error); // Sync error. | ||
} | ||
if (promise) { | ||
// Use setTimeout to break the promise chain. | ||
promise.then(function () { | ||
setTimeout(callback); | ||
}, function (error) { | ||
setTimeout(function () { | ||
callback(error); | ||
}); | ||
}); | ||
} else { | ||
callback(); | ||
} | ||
} | ||
function returnNull() { | ||
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); | ||
}); | ||
return props; | ||
} | ||
var BrowserTransitionHandling = { | ||
handleTransitionError: function (component, error) { | ||
throw error; // This error probably originated in a transition hook. | ||
}, | ||
handleAbortedTransition: function (component, transition) { | ||
var reason = transition.abortReason; | ||
if (reason instanceof Redirect) { | ||
component.replaceWith(reason.to, reason.params, reason.query); | ||
} else { | ||
component.goBack(); | ||
} | ||
} | ||
}; | ||
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'); | ||
/** | ||
* The <Routes> component configures the route hierarchy and renders the | ||
@@ -14,4 +312,203 @@ * route matching the current location when rendered into a document. | ||
mixins: [ TransitionHandler ], | ||
mixins: [ ActiveContext, LocationContext, RouteContext, ScrollContext ], | ||
propTypes: { | ||
initialPath: React.PropTypes.string, | ||
onChange: React.PropTypes.func | ||
}, | ||
getInitialState: function () { | ||
return { | ||
matches: [] | ||
}; | ||
}, | ||
componentWillMount: function () { | ||
this.handlePathChange(this.props.initialPath); | ||
}, | ||
componentDidMount: function () { | ||
PathStore.addChangeListener(this.handlePathChange); | ||
}, | ||
componentWillUnmount: function () { | ||
PathStore.removeChangeListener(this.handlePathChange); | ||
}, | ||
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); | ||
} | ||
}); | ||
}, | ||
/** | ||
* Performs a depth-first search for the first route in the tree that matches on | ||
* the given path. Returns an array of all routes in the tree leading to the one | ||
* that matched in the format { route, params } where params is an object that | ||
* contains the URL parameters relevant to that route. Returns null if no route | ||
* in the tree matches the path. | ||
* | ||
* React.renderComponent( | ||
* <Routes> | ||
* <Route handler={App}> | ||
* <Route name="posts" handler={Posts}/> | ||
* <Route name="post" path="/posts/:id" handler={Post}/> | ||
* </Route> | ||
* </Routes> | ||
* ).match('/posts/123'); => [ { route: <AppRoute>, params: {} }, | ||
* { route: <PostRoute>, params: { id: '123' } } ] | ||
*/ | ||
match: function (path) { | ||
return findMatches(Path.withoutQuery(path), this.getRoutes(), this.props.defaultRoute, this.props.notFoundRoute); | ||
}, | ||
/** | ||
* 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. | ||
* | ||
* 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). | ||
*/ | ||
dispatch: function (path, callback) { | ||
var transition = new Transition(this, path); | ||
var self = this; | ||
computeNextState(this, transition, function (error, nextState) { | ||
if (error || nextState == null) | ||
return callback(error, transition); | ||
self.setState(nextState, function () { | ||
callback(null, transition); | ||
}); | ||
}); | ||
}, | ||
/** | ||
* Returns the props that should be used for the top-level route handler. | ||
*/ | ||
getHandlerProps: function () { | ||
return computeHandlerProps(this.state.matches, this.state.activeQuery); | ||
}, | ||
/** | ||
* Returns a reference to the active route handler's component instance. | ||
*/ | ||
getActiveComponent: function () { | ||
return this.refs.__activeRoute__; | ||
}, | ||
/** | ||
* Returns the current URL path. | ||
*/ | ||
getCurrentPath: function () { | ||
return this.state.path; | ||
}, | ||
/** | ||
* Returns an absolute URL path created from the given route | ||
* name, URL parameters, and query values. | ||
*/ | ||
makePath: function (to, params, query) { | ||
var path; | ||
if (Path.isAbsolute(to)) { | ||
path = Path.normalize(to); | ||
} else { | ||
var namedRoutes = this.getNamedRoutes(); | ||
var route = namedRoutes[to]; | ||
invariant( | ||
route, | ||
'Unable to find a route named "' + to + '". Make sure you have ' + | ||
'a <Route name="' + to + '"> defined somewhere in your <Routes>' | ||
); | ||
path = route.props.path; | ||
} | ||
return Path.withQuery(Path.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. | ||
*/ | ||
makeHref: function (to, params, query) { | ||
var path = this.makePath(to, params, query); | ||
if (this.getLocation() === HashLocation) | ||
return '#' + path; | ||
return path; | ||
}, | ||
/** | ||
* Transitions to the URL specified in the arguments by pushing | ||
* a new URL onto the history stack. | ||
*/ | ||
transitionTo: function (to, params, query) { | ||
var location = this.getLocation(); | ||
invariant( | ||
location, | ||
'You cannot use transitionTo without a location' | ||
); | ||
location.push(this.makePath(to, params, query)); | ||
}, | ||
/** | ||
* Transitions to the URL specified in the arguments by replacing | ||
* the current URL in the history stack. | ||
*/ | ||
replaceWith: function (to, params, query) { | ||
var location = this.getLocation(); | ||
invariant( | ||
location, | ||
'You cannot use replaceWith without a location' | ||
); | ||
location.replace(this.makePath(to, params, query)); | ||
}, | ||
/** | ||
* Transitions to the previous URL. | ||
*/ | ||
goBack: function () { | ||
var location = this.getLocation(); | ||
invariant( | ||
location, | ||
'You cannot use goBack without a location' | ||
); | ||
location.pop(); | ||
}, | ||
render: function () { | ||
@@ -26,2 +523,22 @@ var match = this.state.matches[0]; | ||
); | ||
}, | ||
childContextTypes: { | ||
currentPath: React.PropTypes.string, | ||
makePath: React.PropTypes.func.isRequired, | ||
makeHref: React.PropTypes.func.isRequired, | ||
transitionTo: React.PropTypes.func.isRequired, | ||
replaceWith: React.PropTypes.func.isRequired, | ||
goBack: React.PropTypes.func.isRequired | ||
}, | ||
getChildContext: function () { | ||
return { | ||
currentPath: this.getCurrentPath(), | ||
makePath: this.makePath, | ||
makeHref: this.makeHref, | ||
transitionTo: this.transitionTo, | ||
replaceWith: this.replaceWith, | ||
goBack: this.goBack | ||
}; | ||
} | ||
@@ -28,0 +545,0 @@ |
@@ -9,5 +9,3 @@ exports.DefaultRoute = require('./components/DefaultRoute'); | ||
exports.ActiveState = require('./mixins/ActiveState'); | ||
exports.AsyncState = require('./mixins/AsyncState'); | ||
exports.PathState = require('./mixins/PathState'); | ||
exports.RouteLookup = require('./mixins/RouteLookup'); | ||
exports.Transitions = require('./mixins/Transitions'); | ||
exports.CurrentPath = require('./mixins/CurrentPath'); | ||
exports.Navigation = require('./mixins/Navigation'); |
@@ -31,3 +31,4 @@ var invariant = require('react/lib/invariant'); | ||
// changed. It was probably caused by the user clicking the Back | ||
// button, but may have also been the Forward button. | ||
// button, but may have also been the Forward button or manual | ||
// manipulation. So just guess 'pop'. | ||
type: _actionType || LocationActions.POP, | ||
@@ -34,0 +35,0 @@ path: getHashPath() |
var React = require('react'); | ||
var ActiveDelegate = require('./ActiveDelegate'); | ||
/** | ||
* A mixin for components that need to know about the routes, params, | ||
* and query that are currently active. Components that use it get two | ||
* things: | ||
* | ||
* 1. An `updateActiveState` method that is called when the | ||
* active state changes. | ||
* 2. An `isActive` method they can use to check if a route, | ||
* params, and query are active. | ||
* | ||
* Example: | ||
* | ||
* var Tab = React.createClass({ | ||
* | ||
* mixins: [ Router.ActiveState ], | ||
* | ||
* getInitialState: function () { | ||
* return { | ||
* isActive: false | ||
* }; | ||
* }, | ||
* | ||
* updateActiveState: function () { | ||
* this.setState({ | ||
* isActive: this.isActive(routeName, params, query) | ||
* }) | ||
* } | ||
* | ||
* }); | ||
* A mixin for components that need to know the routes, URL | ||
* params and query that are currently active. | ||
*/ | ||
@@ -37,29 +10,35 @@ var ActiveState = { | ||
contextTypes: { | ||
activeDelegate: React.PropTypes.any.isRequired | ||
activeRoutes: React.PropTypes.array.isRequired, | ||
activeParams: React.PropTypes.object.isRequired, | ||
activeQuery: React.PropTypes.object.isRequired, | ||
isActive: React.PropTypes.func.isRequired | ||
}, | ||
componentWillMount: function () { | ||
if (this.updateActiveState) | ||
this.updateActiveState(); | ||
/** | ||
* Returns an array of the routes that are currently active. | ||
*/ | ||
getActiveRoutes: function () { | ||
return this.context.activeRoutes; | ||
}, | ||
componentDidMount: function () { | ||
this.context.activeDelegate.addChangeListener(this.handleActiveStateChange); | ||
/** | ||
* Returns an object of the URL params that are currently active. | ||
*/ | ||
getActiveParams: function () { | ||
return this.context.activeParams; | ||
}, | ||
componentWillUnmount: function () { | ||
this.context.activeDelegate.removeChangeListener(this.handleActiveStateChange); | ||
/** | ||
* Returns an object of the query params that are currently active. | ||
*/ | ||
getActiveQuery: function () { | ||
return this.context.activeQuery; | ||
}, | ||
handleActiveStateChange: function () { | ||
if (this.isMounted() && this.updateActiveState) | ||
this.updateActiveState(); | ||
}, | ||
/** | ||
* Returns true if the route with the given name, URL parameters, and | ||
* query are all currently active. | ||
* A helper method to determine if a given route, params, and query | ||
* are active. | ||
*/ | ||
isActive: function (routeName, params, query) { | ||
return this.context.activeDelegate.isActive(routeName, params, query); | ||
isActive: function (to, params, query) { | ||
return this.context.isActive(to, params, query); | ||
} | ||
@@ -66,0 +45,0 @@ |
@@ -8,2 +8,3 @@ var assert = require('assert'); | ||
describe('PathStore', function () { | ||
beforeEach(function () { | ||
@@ -101,2 +102,3 @@ LocationDispatcher.handleViewAction({ | ||
}); | ||
}); |
@@ -198,2 +198,8 @@ var expect = require('expect'); | ||
describe('when a pattern has one splat', function () { | ||
it('returns the correct path', function () { | ||
expect(Path.injectParams('/a/*/d', { splat: 'b/c' })).toEqual('/a/b/c/d'); | ||
}); | ||
}); | ||
describe('when a pattern has multiple splats', function () { | ||
@@ -203,2 +209,8 @@ it('returns the correct path', function () { | ||
}); | ||
it('complains if not given enough splat values', function () { | ||
expect(function() { | ||
Path.injectParams('/a/*/c/*', { splat: [ 'b' ] }); | ||
}).toThrow(Error); | ||
}); | ||
}); | ||
@@ -205,0 +217,0 @@ }); |
@@ -11,4 +11,4 @@ var mixInto = require('react/lib/mixInto'); | ||
*/ | ||
function Transition(pathDelegate, path) { | ||
this.pathDelegate = pathDelegate; | ||
function Transition(routesComponent, path) { | ||
this.routesComponent = routesComponent; | ||
this.path = path; | ||
@@ -35,3 +35,3 @@ this.abortReason = null; | ||
retry: function () { | ||
this.pathDelegate.replaceWith(this.path); | ||
this.routesComponent.replaceWith(this.path); | ||
} | ||
@@ -38,0 +38,0 @@ |
{ | ||
"name": "react-router", | ||
"version": "0.8.0", | ||
"version": "0.9.0", | ||
"description": "A complete routing library for React.js", | ||
@@ -5,0 +5,0 @@ "main": "./modules/index", |
21
tests.js
@@ -5,8 +5,19 @@ require('./modules/components/__tests__/DefaultRoute-test'); | ||
require('./modules/components/__tests__/Routes-test'); | ||
require('./modules/mixins/__tests__/ActiveDelegate-test'); | ||
require('./modules/mixins/__tests__/AsyncState-test'); | ||
require('./modules/mixins/__tests__/PathDelegate-test'); | ||
require('./modules/mixins/__tests__/PathState-test'); | ||
require('./modules/mixins/__tests__/RouteContainer-test'); | ||
require('./modules/mixins/__tests__/ActiveContext-test'); | ||
require('./modules/mixins/__tests__/LocationContext-test'); | ||
require('./modules/mixins/__tests__/Navigation-test'); | ||
require('./modules/mixins/__tests__/RouteContext-test'); | ||
require('./modules/mixins/__tests__/ScrollContext-test'); | ||
require('./modules/stores/__tests__/PathStore-test'); | ||
require('./modules/utils/__tests__/Path-test'); | ||
var PathStore = require('./modules/stores/PathStore'); | ||
afterEach(function () { | ||
// For some reason unmountComponentAtNode doesn't call componentWillUnmount :/ | ||
PathStore.removeAllChangeListeners(); | ||
}); |
@@ -8,3 +8,3 @@ Upgrade Guide | ||
0.7.x -> 0.8.x | ||
0.7.x -> 0.9.x | ||
-------------- | ||
@@ -28,3 +28,3 @@ | ||
// 0.8.x | ||
// 0.9.x | ||
var SomethingActive = React.createClass({ | ||
@@ -41,3 +41,3 @@ mixins: [ActiveState], | ||
### `<Routes onActiveStateChange/>` -> `PathState` | ||
### `<Routes onActiveStateChange/>` -> `<Routes onChange />` | ||
@@ -48,7 +48,16 @@ ```js | ||
// 0.8.x | ||
var App = React.createClass({ | ||
mixins: [PathState], | ||
updatePath: fn | ||
}); | ||
function fn(nextState) {} | ||
// 0.9.x | ||
<Routes onActiveStateChange={fn} /> | ||
function fn() { | ||
// no arguments | ||
// `this` is the routes instance | ||
// here are some useful methods to get at the data you probably need | ||
this.getCurrentPath(); | ||
this.getActiveRoutes(); | ||
this.getActiveParams(); | ||
this.getActiveQuery(); | ||
} | ||
``` | ||
@@ -62,22 +71,4 @@ | ||
`.` used to be a delimiter like `/`, but now its a valid character in | ||
your params. If you were using this feature you'll need to do the split | ||
yourself. | ||
your params. | ||
``` | ||
// 0.7.x | ||
var route = <Route path=":foo.:bar" />; | ||
// 0.8.x | ||
var route = <Route path=":foobar" handler={Handler}/> | ||
Handler = React.createClass({ | ||
render: function() { | ||
var split = this.props.params.foobar.split('.'); | ||
var foo = split[0]; | ||
var bar = split[1]; | ||
// ... | ||
} | ||
}); | ||
``` | ||
### `transition.retry()` | ||
@@ -97,5 +88,5 @@ | ||
// 0.8.x | ||
// 0.9.x | ||
React.createClass({ | ||
mixins: [Transitions], | ||
mixins: [Navigation], | ||
login: function() { | ||
@@ -123,3 +114,3 @@ // ... | ||
// 0.8.x | ||
// 0.9.x | ||
React.createClass({ | ||
@@ -139,14 +130,14 @@ statics: { | ||
There are now three scroll behaviors you can use: | ||
- `'imitateBrowser'` | ||
- `'browser'` | ||
- `'scrollToTop'` | ||
- `'none'` | ||
`imitateBrowser` is the default, and imitates what browsers do in a | ||
typical page reload scenario (preserves scroll positions when using the | ||
back button, scrolls up when you come to a new page, etc.) | ||
`browser` is the default, and imitates what browsers do in a typical | ||
page reload scenario (preserves scroll positions when using the back | ||
button, scrolls up when you come to a new page, etc.) Also, you can no | ||
longer specify scroll behavior per `<Route/>` anymore, only `<Routes/>` | ||
Also, you can't specify scroll behavior per `<Route/>` anymore. | ||
``` | ||
@@ -160,3 +151,3 @@ <Routes scrollBehavior="scrollToTop"/> | ||
It's gone now. We have made getting at the current routes incredibly | ||
convenient now with the `RouteLookup` mixin. | ||
convenient now with additions to the `ActiveState` mixin. | ||
@@ -179,5 +170,7 @@ ### `Router.transitionTo, replaceWith, goBack` | ||
// 0.8.x | ||
// 0.9.x | ||
var Navigation = Router.Navigation; | ||
React.createClass({ | ||
mixins: [Router.Transitions], | ||
mixins: [Navigation], | ||
whenever: function() { | ||
@@ -191,4 +184,12 @@ this.transitionTo('something'); | ||
0.7.x -> 0.8.x | ||
-------------- | ||
Please don't upgrade to `0.8.0`, just skip to `0.9.x`. | ||
`0.8.0` had some transient mixins we didn't intend to document, but had | ||
some miscommunication :( If you were one of three people who used some | ||
of these mixins and need help upgrading from `0.8.0 -> 0.9.x` find us on | ||
freenode in `#rackt` or open a ticket. Thanks! | ||
0.6.x -> 0.7.x | ||
@@ -195,0 +196,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
334127
69
6719