,
+ document.getElementById('example')
+);
diff --git a/examples/master-detail/app.js b/examples/master-detail/app.js
index 46ba1c4d75..272e489b10 100644
--- a/examples/master-detail/app.js
+++ b/examples/master-detail/app.js
@@ -2,6 +2,7 @@
var React = require('react');
var Router = require('../../index');
var Route = Router.Route;
+var DefaultRoute = Router.DefaultRoute;
var Routes = Router.Routes;
var Link = Router.Link;
@@ -85,12 +86,10 @@ var App = React.createClass({
},
componentDidMount: function() {
- console.log('componentDidMount')
ContactStore.addChangeListener(this.updateContacts);
},
componentWillUnmount: function () {
- console.log('componentWillUnmount')
ContactStore.removeChangeListener(this.updateContacts);
},
@@ -104,10 +103,6 @@ var App = React.createClass({
});
},
- indexTemplate: function() {
- return Address Book
;
- },
-
render: function() {
var contacts = this.state.contacts.map(function(contact) {
return {contact.first}
@@ -121,13 +116,19 @@ var App = React.createClass({
- {this.props.activeRouteHandler() || this.indexTemplate()}
+ {this.props.activeRouteHandler()}
);
}
});
+var Index = React.createClass({
+ render: function() {
+ return Address Book
;
+ }
+});
+
var Contact = React.createClass({
getInitialState: function() {
return {
@@ -204,16 +205,18 @@ var NotFound = React.createClass({
});
var routes = (
-
-
-
-
-
-
-
+
+
+
+
+
+
);
-React.renderComponent(routes, document.getElementById('example'));
+React.renderComponent(
+ ,
+ document.getElementById('example')
+);
// Request utils.
diff --git a/index.js b/index.js
index 436d957f0e..c2bb0391bd 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,6 @@
exports.ActiveState = require('./ActiveState');
exports.AsyncState = require('./AsyncState');
+exports.DefaultRoute = require('./DefaultRoute');
exports.Link = require('./Link');
exports.Redirect = require('./Redirect');
exports.Route = require('./Route');
diff --git a/modules/components/DefaultRoute.js b/modules/components/DefaultRoute.js
new file mode 100644
index 0000000000..ef281bcc6f
--- /dev/null
+++ b/modules/components/DefaultRoute.js
@@ -0,0 +1,19 @@
+var copyProperties = require('react/lib/copyProperties');
+var Route = require('./Route');
+
+/**
+ * A component is a special kind of that
+ * renders when its parent matches but none of its siblings do.
+ * Only one such route may be used at any given level in the
+ * route hierarchy.
+ */
+function DefaultRoute(props) {
+ return Route(
+ copyProperties(props, {
+ name: null,
+ path: null
+ })
+ );
+}
+
+module.exports = DefaultRoute;
diff --git a/modules/components/Route.js b/modules/components/Route.js
index 14c6679ca3..407e6cf7c7 100644
--- a/modules/components/Route.js
+++ b/modules/components/Route.js
@@ -9,6 +9,8 @@ var withoutProperties = require('../helpers/withoutProperties');
var RESERVED_PROPS = {
handler: true,
path: true,
+ defaultRoute: true,
+ paramNames: true,
children: true // ReactChildren
};
diff --git a/modules/components/Routes.js b/modules/components/Routes.js
index bde17afcba..c279a4fc9d 100644
--- a/modules/components/Routes.js
+++ b/modules/components/Routes.js
@@ -94,7 +94,20 @@ var Routes = React.createClass({
},
getInitialState: function () {
- return {};
+ return {
+ routes: this.getRoutes()
+ };
+ },
+
+ getRoutes: function () {
+ var routes = [];
+
+ React.Children.forEach(this.props.children, function (child) {
+ if (child = RouteStore.registerRoute(child, this))
+ routes.push(child);
+ }, this);
+
+ return routes;
},
getLocation: function () {
@@ -107,12 +120,7 @@ var Routes = React.createClass({
},
componentWillMount: function () {
- React.Children.forEach(this.props.children, function (child) {
- RouteStore.registerRoute(child);
- });
-
PathStore.setup(this.getLocation());
-
PathStore.addChangeListener(this.handlePathChange);
},
@@ -146,15 +154,7 @@ var Routes = React.createClass({
* { route: , params: { id: '123' } } ]
*/
match: function (path) {
- var rootRoutes = this.props.children;
- if (!Array.isArray(rootRoutes)) {
- rootRoutes = [rootRoutes];
- }
- var matches = null;
- for (var i = 0; matches == null && i < rootRoutes.length; i++) {
- matches = findMatches(Path.withoutQuery(path), rootRoutes[i]);
- }
- return matches;
+ return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute);
},
/**
@@ -229,53 +229,42 @@ var Routes = React.createClass({
});
-function findMatches(path,route){
- var matches = null;
+function findMatches(path, routes, defaultRoute) {
+ var matches = null, route, params;
- if (Array.isArray(route)) {
- for (var i = 0, len = route.length; matches == null && i < len; ++i) {
- matches = findMatches(path, route[i]);
- }
- } else {
- matches = findMatchesForRoute(path,route);
- }
-
- return matches;
-}
+ for (var i = 0, len = routes.length; i < len; ++i) {
+ route = routes[i];
-function findMatchesForRoute(path, route) {
- var children = route.props.children, matches;
- var params;
+ // Check the subtree first to find the most deeply-nested match.
+ matches = findMatches(path, route.props.children, route.props.defaultRoute);
- // Check the subtree first to find the most deeply-nested match.
- if (Array.isArray(children)) {
- for (var i = 0, len = children.length; matches == null && i < len; ++i) {
- matches = findMatches(path, children[i]);
- }
- } else if (children) {
- matches = findMatches(path, children);
- }
+ if (matches != null) {
+ var rootParams = getRootMatch(matches).params;
+
+ params = route.props.paramNames.reduce(function (params, paramName) {
+ params[paramName] = rootParams[paramName];
+ return params;
+ }, {});
- if (matches) {
- var rootParams = getRootMatch(matches).params;
- params = {};
+ matches.unshift(makeMatch(route, params));
- Path.extractParamNames(route.props.path).forEach(function (paramName) {
- params[paramName] = rootParams[paramName];
- });
+ return matches;
+ }
- matches.unshift(makeMatch(route, params));
+ // No routes in the subtree matched, so check this route.
+ params = Path.extractParams(route.props.path, path);
- return matches;
+ if (params)
+ return [ makeMatch(route, params) ];
}
- // No routes in the subtree matched, so check this route.
- params = Path.extractParams(route.props.path, path);
+ // No routes matched, so try the default route if there is one.
+ params = defaultRoute && Path.extractParams(defaultRoute.props.path, path);
if (params)
- return [ makeMatch(route, params) ];
+ return [ makeMatch(defaultRoute, params) ];
- return null;
+ return matches;
}
function makeMatch(route, params) {
diff --git a/modules/stores/RouteStore.js b/modules/stores/RouteStore.js
index 16ba3105a8..7525ed5304 100644
--- a/modules/stores/RouteStore.js
+++ b/modules/stores/RouteStore.js
@@ -26,12 +26,12 @@ var RouteStore = {
* from the store.
*/
unregisterRoute: function (route) {
- if (route.props.name)
- delete _namedRoutes[route.props.name];
+ var props = route.props;
- React.Children.forEach(route.props.children, function (child) {
- RouteStore.unregisterRoute(child);
- });
+ if (props.name)
+ delete _namedRoutes[props.name];
+
+ React.Children.forEach(props.children, RouteStore.unregisterRoute);
},
/**
@@ -39,50 +39,76 @@ var RouteStore = {
* does some normalization and validation on route props.
*/
registerRoute: function (route, _parentRoute) {
- // Make sure the 's path begins with a slash. Default to its name.
- // We can't do this in getDefaultProps because it may not be called on
- // s that are never actually mounted.
- if (route.props.path || route.props.name) {
- route.props.path = Path.normalize(route.props.path || route.props.name);
- } else {
- route.props.path = '/';
- }
+ // Note: When route is a top-level route, _parentRoute
+ // is actually a , not a . We do this so
+ // can get a defaultRoute like does.
+ var props = route.props;
- // Make sure the has a valid React component for a handler.
invariant(
- React.isValidClass(route.props.handler),
- 'The handler for Route "' + (route.props.name || route.props.path) + '" ' +
- 'must be a valid React component'
+ React.isValidClass(props.handler),
+ 'The handler for the "%s" route must be a valid React class',
+ props.name || props.path
);
- // Make sure the has all params that its parent needs.
- if (_parentRoute) {
- var paramNames = Path.extractParamNames(route.props.path);
+ // Default routes have no name, path, or children.
+ var isDefault = !(props.path || props.name || props.children);
- Path.extractParamNames(_parentRoute.props.path).forEach(function (paramName) {
+ if (props.path || props.name) {
+ props.path = Path.normalize(props.path || props.name);
+ } else if (_parentRoute && _parentRoute.props.path) {
+ props.path = _parentRoute.props.path;
+ } else {
+ props.path = '/';
+ }
+
+ props.paramNames = Path.extractParamNames(props.path);
+
+ // Make sure the route's path has all params its parent needs.
+ if (_parentRoute && Array.isArray(_parentRoute.props.paramNames)) {
+ _parentRoute.props.paramNames.forEach(function (paramName) {
invariant(
- paramNames.indexOf(paramName) !== -1,
- 'The nested route path "' + route.props.path + '" is missing the "' + paramName + '" ' +
- 'parameter of its parent path "' + _parentRoute.props.path + '"'
+ props.paramNames.indexOf(paramName) !== -1,
+ 'The nested route path "%s" is missing the "%s" parameter of its parent path "%s"',
+ props.path, paramName, _parentRoute.props.path
);
});
}
- // Make sure the can be looked up by s.
- if (route.props.name) {
- var existingRoute = _namedRoutes[route.props.name];
+ // Make sure the route can be looked up by s.
+ if (props.name) {
+ var existingRoute = _namedRoutes[props.name];
invariant(
!existingRoute || route === existingRoute,
- 'You cannot use the name "' + route.props.name + '" for more than one route'
+ 'You cannot use the name "%s" for more than one route',
+ props.name
);
- _namedRoutes[route.props.name] = route;
+ _namedRoutes[props.name] = route;
}
- React.Children.forEach(route.props.children, function (child) {
- RouteStore.registerRoute(child, route);
+ if (_parentRoute && isDefault) {
+ invariant(
+ _parentRoute.props.defaultRoute == null,
+ 'You may not have more than one per '
+ );
+
+ _parentRoute.props.defaultRoute = route;
+
+ return null;
+ }
+
+ // Make sure children is an array, excluding s.
+ var children = [];
+
+ React.Children.forEach(props.children, function (child) {
+ if (child = RouteStore.registerRoute(child, route))
+ children.push(child);
});
+
+ props.children = children;
+
+ return route;
},
/**
diff --git a/specs/DefaultRoute.spec.js b/specs/DefaultRoute.spec.js
new file mode 100644
index 0000000000..23929e946e
--- /dev/null
+++ b/specs/DefaultRoute.spec.js
@@ -0,0 +1,64 @@
+require('./helper');
+var RouteStore = require('../modules/stores/RouteStore');
+var DefaultRoute = require('../modules/components/DefaultRoute');
+var Route = require('../modules/components/Route');
+var Routes = require('../modules/components/Routes');
+
+var App = React.createClass({
+ displayName: 'App',
+ render: function () {
+ return React.DOM.div();
+ }
+});
+
+describe('when registering a DefaultRoute', function () {
+ describe('nested inside a Route component', function () {
+ it('becomes that Route\'s defaultRoute', function () {
+ var defaultRoute;
+ var route = Route({ handler: App },
+ defaultRoute = DefaultRoute({ handler: App })
+ );
+
+ RouteStore.registerRoute(route);
+ expect(route.props.defaultRoute).toBe(defaultRoute);
+ RouteStore.unregisterRoute(route);
+ });
+ });
+
+ describe('nested inside a Routes component', function () {
+ it('becomes that Routes\' defaultRoute', function () {
+ var defaultRoute;
+ var routes = Routes({ handler: App },
+ defaultRoute = DefaultRoute({ handler: App })
+ );
+
+ RouteStore.registerRoute(defaultRoute, routes);
+ expect(routes.props.defaultRoute).toBe(defaultRoute);
+ RouteStore.unregisterRoute(defaultRoute);
+ });
+ });
+});
+
+describe('when no child routes match a URL, but the parent matches', function () {
+ it('matches the default route', function () {
+ var defaultRoute;
+ var routes = ReactTestUtils.renderIntoDocument(
+ Routes(null,
+ Route({ name: 'user', path: '/users/:id', handler: App },
+ Route({ name: 'home', path: '/users/:id/home', handler: App }),
+ // Make it the middle sibling to test order independence.
+ defaultRoute = DefaultRoute({ handler: App }),
+ Route({ name: 'news', path: '/users/:id/news', handler: App })
+ )
+ )
+ );
+
+ var matches = routes.match('/users/5');
+ assert(matches);
+ expect(matches.length).toEqual(2);
+
+ expect(matches[1].route).toBe(defaultRoute);
+
+ expect(matches[0].route.props.name).toEqual('user');
+ });
+});
diff --git a/specs/PathStore.spec.js b/specs/PathStore.spec.js
index 9df3b645fd..cad4230dd0 100644
--- a/specs/PathStore.spec.js
+++ b/specs/PathStore.spec.js
@@ -48,7 +48,6 @@ describe('PathStore', function () {
it('has the correct path', function () {
expect(PathStore.getCurrentPath()).toEqual('/one');
});
-
});
});
diff --git a/specs/Route.spec.js b/specs/Route.spec.js
index f255b63b25..61361a2d8b 100644
--- a/specs/Route.spec.js
+++ b/specs/Route.spec.js
@@ -71,7 +71,7 @@ describe('a nested Route that matches the URL', function () {
Routes(null,
Route({ handler: App },
Route({ name: 'posts', path: '/posts/:id', handler: App },
- Route({ name: 'comment', path: '/posts/:id/comments/:commentId', handler: App })
+ Route({ name: 'comment', path: '/posts/:id/comments/:commentID', handler: App })
)
)
)
@@ -83,7 +83,7 @@ describe('a nested Route that matches the URL', function () {
var rootMatch = getRootMatch(matches);
expect(rootMatch.route.props.name).toEqual('comment');
- expect(rootMatch.params).toEqual({ id: 'abc', commentId: '123' });
+ expect(rootMatch.params).toEqual({ id: 'abc', commentID: '123' });
var postsMatch = matches[1];
expect(postsMatch.route.props.name).toEqual('posts');
diff --git a/specs/RouteStore.spec.js b/specs/RouteStore.spec.js
index 614fc7b656..ded71e5b14 100644
--- a/specs/RouteStore.spec.js
+++ b/specs/RouteStore.spec.js
@@ -43,6 +43,19 @@ describe('when registering a route', function () {
expect(route.props.path).toEqual('/');
RouteStore.unregisterRoute(route);
});
+
+ describe('that is nested inside another route', function () {
+ it('uses the parent\'s path', function () {
+ var child;
+ var route = Route({ name: 'home', handler: App },
+ child = Route({ handler: App })
+ );
+
+ RouteStore.registerRoute(route);
+ expect(child.props.path).toEqual(route.props.path);
+ RouteStore.unregisterRoute(route);
+ });
+ });
});
describe('with a name but no path', function () {
diff --git a/specs/main.js b/specs/main.js
index d7f80e3d62..4c6c5cb003 100644
--- a/specs/main.js
+++ b/specs/main.js
@@ -2,6 +2,7 @@
// for every spec file, there must be some sort of config but I can't find it ...
require('./ActiveStore.spec.js');
require('./AsyncState.spec.js');
+require('./DefaultRoute.spec.js');
require('./Path.spec.js');
require('./PathStore.spec.js');
require('./Route.spec.js');