Skip to content

Commit

Permalink
[added] <DefaultRoute> component
Browse files Browse the repository at this point in the history
Also, changed behavior of routes with no name, path, or children so
they act as default routes. <DefaultRoute> is essentially just sugar.

Fixes #164
Fixes #193
  • Loading branch information
mjackson committed Aug 14, 2014
1 parent cabc759 commit e028768
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 106 deletions.
1 change: 1 addition & 0 deletions DefaultRoute.js
@@ -0,0 +1 @@
module.exports = require('./modules/components/DefaultRoute');
17 changes: 9 additions & 8 deletions examples/dynamic-segments/app.js
Expand Up @@ -47,14 +47,15 @@ var Task = React.createClass({
});

var routes = (
<Routes>
<Route handler={App}>
<Route name="user" path="/user/:userId" handler={User}>
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
</Route>
<Route handler={App}>
<Route name="user" path="/user/:userId" handler={User}>
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
</Route>
</Routes>
</Route>
);

React.renderComponent(routes, document.getElementById('example'));
React.renderComponent(
<Routes children={routes}/>,
document.getElementById('example')
);
33 changes: 18 additions & 15 deletions examples/master-detail/app.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
},

Expand All @@ -104,10 +103,6 @@ var App = React.createClass({
});
},

indexTemplate: function() {
return <h1>Address Book</h1>;
},

render: function() {
var contacts = this.state.contacts.map(function(contact) {
return <li key={contact.id}><Link to="contact" id={contact.id}>{contact.first}</Link></li>
Expand All @@ -121,13 +116,19 @@ var App = React.createClass({
</ul>
</div>
<div className="Content">
{this.props.activeRouteHandler() || this.indexTemplate()}
{this.props.activeRouteHandler()}
</div>
</div>
);
}
});

var Index = React.createClass({
render: function() {
return <h1>Address Book</h1>;
}
});

var Contact = React.createClass({
getInitialState: function() {
return {
Expand Down Expand Up @@ -204,16 +205,18 @@ var NotFound = React.createClass({
});

var routes = (
<Routes>
<Route handler={App}>
<Route name="new" path="contact/new" handler={NewContact}/>
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
<Route name="contact" path="contact/:id" handler={Contact}/>
</Route>
</Routes>
<Route handler={App}>
<DefaultRoute handler={Index}/>
<Route name="new" path="contact/new" handler={NewContact}/>
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
<Route name="contact" path="contact/:id" handler={Contact}/>
</Route>
);

React.renderComponent(routes, document.getElementById('example'));
React.renderComponent(
<Routes children={routes}/>,
document.getElementById('example')
);

// Request utils.

Expand Down
1 change: 1 addition & 0 deletions 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');
Expand Down
19 changes: 19 additions & 0 deletions modules/components/DefaultRoute.js
@@ -0,0 +1,19 @@
var copyProperties = require('react/lib/copyProperties');
var Route = require('./Route');

/**
* A <DefaultRoute> component is a special kind of <Route> 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;
2 changes: 2 additions & 0 deletions modules/components/Route.js
Expand Up @@ -9,6 +9,8 @@ var withoutProperties = require('../helpers/withoutProperties');
var RESERVED_PROPS = {
handler: true,
path: true,
defaultRoute: true,
paramNames: true,
children: true // ReactChildren
};

Expand Down
89 changes: 39 additions & 50 deletions modules/components/Routes.js
Expand Up @@ -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 () {
Expand All @@ -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);
},

Expand Down Expand Up @@ -146,15 +154,7 @@ var Routes = React.createClass({
* { route: <PostRoute>, 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);
},

/**
Expand Down Expand Up @@ -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) {
Expand Down
88 changes: 57 additions & 31 deletions modules/stores/RouteStore.js
Expand Up @@ -26,63 +26,89 @@ 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);
},

/**
* Registers a <Route> and all of its children with the store. Also,
* does some normalization and validation on route props.
*/
registerRoute: function (route, _parentRoute) {
// Make sure the <Route>'s path begins with a slash. Default to its name.
// We can't do this in getDefaultProps because it may not be called on
// <Route>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 <Routes>, not a <Route>. We do this so
// <Routes> can get a defaultRoute like <Route> does.
var props = route.props;

// Make sure the <Route> 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 <Route> 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 <Route> can be looked up by <Link>s.
if (route.props.name) {
var existingRoute = _namedRoutes[route.props.name];
// Make sure the route can be looked up by <Link>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 <DefaultRoute> per <Route>'
);

_parentRoute.props.defaultRoute = route;

return null;
}

// Make sure children is an array, excluding <DefaultRoute>s.
var children = [];

React.Children.forEach(props.children, function (child) {
if (child = RouteStore.registerRoute(child, route))
children.push(child);
});

props.children = children;

return route;
},

/**
Expand Down

0 comments on commit e028768

Please sign in to comment.