Skip to content

Commit

Permalink
[added] <NotFoundRoute>
Browse files Browse the repository at this point in the history
Fixes #140
  • Loading branch information
mjackson authored and ryanflorence committed Aug 27, 2014
1 parent d5bd656 commit a63c940
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 15 deletions.
1 change: 1 addition & 0 deletions NotFoundRoute.js
@@ -0,0 +1 @@
module.exports = require('./modules/components/NotFoundRoute');
4 changes: 3 additions & 1 deletion examples/master-detail/app.js
Expand Up @@ -5,6 +5,7 @@ var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;
var Routes = Router.Routes;
var Link = Router.Link;
var NotFoundRoute = Router.NotFoundRoute;

var api = 'http://addressbook-api.herokuapp.com/contacts';
var _contacts = {};
Expand Down Expand Up @@ -114,6 +115,7 @@ var App = React.createClass({
<ul>
{contacts}
</ul>
<Link to="/nothing-here">Invalid Link (not found)</Link>
</div>
<div className="Content">
{this.props.activeRouteHandler()}
Expand Down Expand Up @@ -247,8 +249,8 @@ var 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}/>
<NotFoundRoute handler={NotFound}/>
</Route>
);

Expand Down
3 changes: 2 additions & 1 deletion index.js
Expand Up @@ -2,10 +2,11 @@ exports.ActiveState = require('./ActiveState');
exports.AsyncState = require('./AsyncState');
exports.DefaultRoute = require('./DefaultRoute');
exports.Link = require('./Link');
exports.NotFoundRoute = require('./NotFoundRoute');
exports.Redirect = require('./Redirect');
exports.Route = require('./Route');
exports.Routes = require('./Routes');
exports.goBack = require('./goBack');
exports.makeHref = require('./makeHref');
exports.replaceWith = require('./replaceWith');
exports.transitionTo = require('./transitionTo');
exports.makeHref = require('./makeHref');
13 changes: 13 additions & 0 deletions modules/components/NotFoundRoute.js
@@ -0,0 +1,13 @@
var merge = require('react/lib/merge');
var Route = require('./Route');

function NotFoundRoute(props) {
return Route(
merge(props, {
path: null,
catchAll: true
})
);
}

module.exports = NotFoundRoute;
14 changes: 8 additions & 6 deletions modules/components/Routes.js
Expand Up @@ -143,7 +143,7 @@ var Routes = React.createClass({
* { route: <PostRoute>, params: { id: '123' } } ]
*/
match: function (path) {
return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute);
return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute, this.props.notFoundRoute);
},

/**
Expand Down Expand Up @@ -218,14 +218,14 @@ var Routes = React.createClass({

});

function findMatches(path, routes, defaultRoute) {
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);
matches = findMatches(path, route.props.children, route.props.defaultRoute, route.props.notFoundRoute);

if (matches != null) {
var rootParams = getRootMatch(matches).params;
Expand All @@ -248,11 +248,13 @@ function findMatches(path, routes, defaultRoute) {
}

// No routes matched, so try the default route if there is one.
params = defaultRoute && Path.extractParams(defaultRoute.props.path, path);

if (params)
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;
}

Expand Down
39 changes: 32 additions & 7 deletions modules/stores/RouteStore.js
Expand Up @@ -48,13 +48,17 @@ var RouteStore = {
props.name || props.path
);

// Default routes have no name, path, or children.
var isDefault = !(props.path || props.name || props.children);

if (props.path || props.name) {
if ((props.path || props.name) && !props.catchAll) {
props.path = Path.normalize(props.path || props.name);
} else if (parentRoute && parentRoute.props.path) {
props.path = parentRoute.props.path;
} else if (parentRoute) {
// <Routes> have no path prop.
props.path = parentRoute.props.path || '/';

if (props.catchAll) {
props.path += '*';
} else if (!props.children) {
props.isDefault = true;
}
} else {
props.path = '/';
}
Expand Down Expand Up @@ -85,7 +89,28 @@ var RouteStore = {
_namedRoutes[props.name] = route;
}

if (parentRoute && isDefault) {
if (props.catchAll) {
invariant(
parentRoute,
'<NotFoundRoute> must have a parent <Route>'
);

invariant(
parentRoute.props.notFoundRoute == null,
'You may not have more than one <NotFoundRoute> per <Route>'
);

parentRoute.props.notFoundRoute = route;

return null;
}

if (props.isDefault) {
invariant(
parentRoute,
'<DefaultRoute> must have a parent <Route>'
);

invariant(
parentRoute.props.defaultRoute == null,
'You may not have more than one <DefaultRoute> per <Route>'
Expand Down
64 changes: 64 additions & 0 deletions specs/NotFoundRoute.spec.js
@@ -0,0 +1,64 @@
require('./helper');
var RouteStore = require('../modules/stores/RouteStore');
var NotFoundRoute = require('../modules/components/NotFoundRoute');
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 NotFoundRoute', function () {
describe('nested inside a Route component', function () {
it('becomes that Route\'s notFoundRoute', function () {
var notFoundRoute;
var route = Route({ handler: App },
notFoundRoute = NotFoundRoute({ handler: App })
);

RouteStore.registerRoute(route);
expect(route.props.notFoundRoute).toBe(notFoundRoute);
RouteStore.unregisterRoute(route);
});
});

describe('nested inside a Routes component', function () {
it('becomes that Routes\' notFoundRoute', function () {
var notFoundRoute;
var routes = Routes({ handler: App },
notFoundRoute = NotFoundRoute({ handler: App })
);

RouteStore.registerRoute(notFoundRoute, routes);
expect(routes.props.notFoundRoute).toBe(notFoundRoute);
RouteStore.unregisterRoute(notFoundRoute);
});
});
});

describe('when no child routes match a URL, but the beginning of the parent\'s path matches', function () {
it('matches the default route', function () {
var notFoundRoute;
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.
notFoundRoute = NotFoundRoute({ handler: App }),
Route({ name: 'news', path: '/users/:id/news', handler: App })
)
)
);

var matches = routes.match('/users/5/not-found');
assert(matches);
expect(matches.length).toEqual(2);

expect(matches[1].route).toBe(notFoundRoute);

expect(matches[0].route.props.name).toEqual('user');
});
});
1 change: 1 addition & 0 deletions specs/main.js
Expand Up @@ -3,6 +3,7 @@
require('./ActiveStore.spec.js');
require('./AsyncState.spec.js');
require('./DefaultRoute.spec.js');
require('./NotFoundRoute.spec.js');
require('./Path.spec.js');
require('./PathStore.spec.js');
require('./Route.spec.js');
Expand Down

0 comments on commit a63c940

Please sign in to comment.