From a63c940621f9134f303cc08166311ec2be07de55 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 19 Aug 2014 12:24:57 -0700 Subject: [PATCH] [added] Fixes #140 --- NotFoundRoute.js | 1 + examples/master-detail/app.js | 4 +- index.js | 3 +- modules/components/NotFoundRoute.js | 13 ++++++ modules/components/Routes.js | 14 ++++--- modules/stores/RouteStore.js | 39 ++++++++++++++---- specs/NotFoundRoute.spec.js | 64 +++++++++++++++++++++++++++++ specs/main.js | 1 + 8 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 NotFoundRoute.js create mode 100644 modules/components/NotFoundRoute.js create mode 100644 specs/NotFoundRoute.spec.js diff --git a/NotFoundRoute.js b/NotFoundRoute.js new file mode 100644 index 0000000000..76f2d6950c --- /dev/null +++ b/NotFoundRoute.js @@ -0,0 +1 @@ +module.exports = require('./modules/components/NotFoundRoute'); diff --git a/examples/master-detail/app.js b/examples/master-detail/app.js index e6f1cbc289..b5df96dfe6 100644 --- a/examples/master-detail/app.js +++ b/examples/master-detail/app.js @@ -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 = {}; @@ -114,6 +115,7 @@ var App = React.createClass({ + Invalid Link (not found)
{this.props.activeRouteHandler()} @@ -247,8 +249,8 @@ var routes = ( - + ); diff --git a/index.js b/index.js index c2bb0391bd..b6a70293e2 100644 --- a/index.js +++ b/index.js @@ -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'); diff --git a/modules/components/NotFoundRoute.js b/modules/components/NotFoundRoute.js new file mode 100644 index 0000000000..1da81ff389 --- /dev/null +++ b/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; diff --git a/modules/components/Routes.js b/modules/components/Routes.js index 44d1188587..2361c40d45 100644 --- a/modules/components/Routes.js +++ b/modules/components/Routes.js @@ -143,7 +143,7 @@ var Routes = React.createClass({ * { route: , 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); }, /** @@ -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; @@ -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; } diff --git a/modules/stores/RouteStore.js b/modules/stores/RouteStore.js index 26dae5bd94..15ba3b289b 100644 --- a/modules/stores/RouteStore.js +++ b/modules/stores/RouteStore.js @@ -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) { + // have no path prop. + props.path = parentRoute.props.path || '/'; + + if (props.catchAll) { + props.path += '*'; + } else if (!props.children) { + props.isDefault = true; + } } else { props.path = '/'; } @@ -85,7 +89,28 @@ var RouteStore = { _namedRoutes[props.name] = route; } - if (parentRoute && isDefault) { + if (props.catchAll) { + invariant( + parentRoute, + ' must have a parent ' + ); + + invariant( + parentRoute.props.notFoundRoute == null, + 'You may not have more than one per ' + ); + + parentRoute.props.notFoundRoute = route; + + return null; + } + + if (props.isDefault) { + invariant( + parentRoute, + ' must have a parent ' + ); + invariant( parentRoute.props.defaultRoute == null, 'You may not have more than one per ' diff --git a/specs/NotFoundRoute.spec.js b/specs/NotFoundRoute.spec.js new file mode 100644 index 0000000000..41f8bf7817 --- /dev/null +++ b/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'); + }); +}); diff --git a/specs/main.js b/specs/main.js index 4c6c5cb003..29fa8639a1 100644 --- a/specs/main.js +++ b/specs/main.js @@ -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');