Skip to content

Commit e028768

Browse files
committed
[added] <DefaultRoute> component
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
1 parent cabc759 commit e028768

File tree

12 files changed

+228
-106
lines changed

12 files changed

+228
-106
lines changed

DefaultRoute.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./modules/components/DefaultRoute');

examples/dynamic-segments/app.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,15 @@ var Task = React.createClass({
4747
});
4848

4949
var routes = (
50-
<Routes>
51-
<Route handler={App}>
52-
<Route name="user" path="/user/:userId" handler={User}>
53-
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
54-
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
55-
</Route>
50+
<Route handler={App}>
51+
<Route name="user" path="/user/:userId" handler={User}>
52+
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
53+
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
5654
</Route>
57-
</Routes>
55+
</Route>
5856
);
5957

60-
React.renderComponent(routes, document.getElementById('example'));
58+
React.renderComponent(
59+
<Routes children={routes}/>,
60+
document.getElementById('example')
61+
);

examples/master-detail/app.js

+18-15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
var React = require('react');
33
var Router = require('../../index');
44
var Route = Router.Route;
5+
var DefaultRoute = Router.DefaultRoute;
56
var Routes = Router.Routes;
67
var Link = Router.Link;
78

@@ -85,12 +86,10 @@ var App = React.createClass({
8586
},
8687

8788
componentDidMount: function() {
88-
console.log('componentDidMount')
8989
ContactStore.addChangeListener(this.updateContacts);
9090
},
9191

9292
componentWillUnmount: function () {
93-
console.log('componentWillUnmount')
9493
ContactStore.removeChangeListener(this.updateContacts);
9594
},
9695

@@ -104,10 +103,6 @@ var App = React.createClass({
104103
});
105104
},
106105

107-
indexTemplate: function() {
108-
return <h1>Address Book</h1>;
109-
},
110-
111106
render: function() {
112107
var contacts = this.state.contacts.map(function(contact) {
113108
return <li key={contact.id}><Link to="contact" id={contact.id}>{contact.first}</Link></li>
@@ -121,13 +116,19 @@ var App = React.createClass({
121116
</ul>
122117
</div>
123118
<div className="Content">
124-
{this.props.activeRouteHandler() || this.indexTemplate()}
119+
{this.props.activeRouteHandler()}
125120
</div>
126121
</div>
127122
);
128123
}
129124
});
130125

126+
var Index = React.createClass({
127+
render: function() {
128+
return <h1>Address Book</h1>;
129+
}
130+
});
131+
131132
var Contact = React.createClass({
132133
getInitialState: function() {
133134
return {
@@ -204,16 +205,18 @@ var NotFound = React.createClass({
204205
});
205206

206207
var routes = (
207-
<Routes>
208-
<Route handler={App}>
209-
<Route name="new" path="contact/new" handler={NewContact}/>
210-
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
211-
<Route name="contact" path="contact/:id" handler={Contact}/>
212-
</Route>
213-
</Routes>
208+
<Route handler={App}>
209+
<DefaultRoute handler={Index}/>
210+
<Route name="new" path="contact/new" handler={NewContact}/>
211+
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
212+
<Route name="contact" path="contact/:id" handler={Contact}/>
213+
</Route>
214214
);
215215

216-
React.renderComponent(routes, document.getElementById('example'));
216+
React.renderComponent(
217+
<Routes children={routes}/>,
218+
document.getElementById('example')
219+
);
217220

218221
// Request utils.
219222

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
exports.ActiveState = require('./ActiveState');
22
exports.AsyncState = require('./AsyncState');
3+
exports.DefaultRoute = require('./DefaultRoute');
34
exports.Link = require('./Link');
45
exports.Redirect = require('./Redirect');
56
exports.Route = require('./Route');

modules/components/DefaultRoute.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
var copyProperties = require('react/lib/copyProperties');
2+
var Route = require('./Route');
3+
4+
/**
5+
* A <DefaultRoute> component is a special kind of <Route> that
6+
* renders when its parent matches but none of its siblings do.
7+
* Only one such route may be used at any given level in the
8+
* route hierarchy.
9+
*/
10+
function DefaultRoute(props) {
11+
return Route(
12+
copyProperties(props, {
13+
name: null,
14+
path: null
15+
})
16+
);
17+
}
18+
19+
module.exports = DefaultRoute;

modules/components/Route.js

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ var withoutProperties = require('../helpers/withoutProperties');
99
var RESERVED_PROPS = {
1010
handler: true,
1111
path: true,
12+
defaultRoute: true,
13+
paramNames: true,
1214
children: true // ReactChildren
1315
};
1416

modules/components/Routes.js

+39-50
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,20 @@ var Routes = React.createClass({
9494
},
9595

9696
getInitialState: function () {
97-
return {};
97+
return {
98+
routes: this.getRoutes()
99+
};
100+
},
101+
102+
getRoutes: function () {
103+
var routes = [];
104+
105+
React.Children.forEach(this.props.children, function (child) {
106+
if (child = RouteStore.registerRoute(child, this))
107+
routes.push(child);
108+
}, this);
109+
110+
return routes;
98111
},
99112

100113
getLocation: function () {
@@ -107,12 +120,7 @@ var Routes = React.createClass({
107120
},
108121

109122
componentWillMount: function () {
110-
React.Children.forEach(this.props.children, function (child) {
111-
RouteStore.registerRoute(child);
112-
});
113-
114123
PathStore.setup(this.getLocation());
115-
116124
PathStore.addChangeListener(this.handlePathChange);
117125
},
118126

@@ -146,15 +154,7 @@ var Routes = React.createClass({
146154
* { route: <PostRoute>, params: { id: '123' } } ]
147155
*/
148156
match: function (path) {
149-
var rootRoutes = this.props.children;
150-
if (!Array.isArray(rootRoutes)) {
151-
rootRoutes = [rootRoutes];
152-
}
153-
var matches = null;
154-
for (var i = 0; matches == null && i < rootRoutes.length; i++) {
155-
matches = findMatches(Path.withoutQuery(path), rootRoutes[i]);
156-
}
157-
return matches;
157+
return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute);
158158
},
159159

160160
/**
@@ -229,53 +229,42 @@ var Routes = React.createClass({
229229

230230
});
231231

232-
function findMatches(path,route){
233-
var matches = null;
232+
function findMatches(path, routes, defaultRoute) {
233+
var matches = null, route, params;
234234

235-
if (Array.isArray(route)) {
236-
for (var i = 0, len = route.length; matches == null && i < len; ++i) {
237-
matches = findMatches(path, route[i]);
238-
}
239-
} else {
240-
matches = findMatchesForRoute(path,route);
241-
}
242-
243-
return matches;
244-
}
235+
for (var i = 0, len = routes.length; i < len; ++i) {
236+
route = routes[i];
245237

246-
function findMatchesForRoute(path, route) {
247-
var children = route.props.children, matches;
248-
var params;
238+
// Check the subtree first to find the most deeply-nested match.
239+
matches = findMatches(path, route.props.children, route.props.defaultRoute);
249240

250-
// Check the subtree first to find the most deeply-nested match.
251-
if (Array.isArray(children)) {
252-
for (var i = 0, len = children.length; matches == null && i < len; ++i) {
253-
matches = findMatches(path, children[i]);
254-
}
255-
} else if (children) {
256-
matches = findMatches(path, children);
257-
}
241+
if (matches != null) {
242+
var rootParams = getRootMatch(matches).params;
243+
244+
params = route.props.paramNames.reduce(function (params, paramName) {
245+
params[paramName] = rootParams[paramName];
246+
return params;
247+
}, {});
258248

259-
if (matches) {
260-
var rootParams = getRootMatch(matches).params;
261-
params = {};
249+
matches.unshift(makeMatch(route, params));
262250

263-
Path.extractParamNames(route.props.path).forEach(function (paramName) {
264-
params[paramName] = rootParams[paramName];
265-
});
251+
return matches;
252+
}
266253

267-
matches.unshift(makeMatch(route, params));
254+
// No routes in the subtree matched, so check this route.
255+
params = Path.extractParams(route.props.path, path);
268256

269-
return matches;
257+
if (params)
258+
return [ makeMatch(route, params) ];
270259
}
271260

272-
// No routes in the subtree matched, so check this route.
273-
params = Path.extractParams(route.props.path, path);
261+
// No routes matched, so try the default route if there is one.
262+
params = defaultRoute && Path.extractParams(defaultRoute.props.path, path);
274263

275264
if (params)
276-
return [ makeMatch(route, params) ];
265+
return [ makeMatch(defaultRoute, params) ];
277266

278-
return null;
267+
return matches;
279268
}
280269

281270
function makeMatch(route, params) {

modules/stores/RouteStore.js

+57-31
Original file line numberDiff line numberDiff line change
@@ -26,63 +26,89 @@ var RouteStore = {
2626
* from the store.
2727
*/
2828
unregisterRoute: function (route) {
29-
if (route.props.name)
30-
delete _namedRoutes[route.props.name];
29+
var props = route.props;
3130

32-
React.Children.forEach(route.props.children, function (child) {
33-
RouteStore.unregisterRoute(child);
34-
});
31+
if (props.name)
32+
delete _namedRoutes[props.name];
33+
34+
React.Children.forEach(props.children, RouteStore.unregisterRoute);
3535
},
3636

3737
/**
3838
* Registers a <Route> and all of its children with the store. Also,
3939
* does some normalization and validation on route props.
4040
*/
4141
registerRoute: function (route, _parentRoute) {
42-
// Make sure the <Route>'s path begins with a slash. Default to its name.
43-
// We can't do this in getDefaultProps because it may not be called on
44-
// <Route>s that are never actually mounted.
45-
if (route.props.path || route.props.name) {
46-
route.props.path = Path.normalize(route.props.path || route.props.name);
47-
} else {
48-
route.props.path = '/';
49-
}
42+
// Note: When route is a top-level route, _parentRoute
43+
// is actually a <Routes>, not a <Route>. We do this so
44+
// <Routes> can get a defaultRoute like <Route> does.
45+
var props = route.props;
5046

51-
// Make sure the <Route> has a valid React component for a handler.
5247
invariant(
53-
React.isValidClass(route.props.handler),
54-
'The handler for Route "' + (route.props.name || route.props.path) + '" ' +
55-
'must be a valid React component'
48+
React.isValidClass(props.handler),
49+
'The handler for the "%s" route must be a valid React class',
50+
props.name || props.path
5651
);
5752

58-
// Make sure the <Route> has all params that its parent needs.
59-
if (_parentRoute) {
60-
var paramNames = Path.extractParamNames(route.props.path);
53+
// Default routes have no name, path, or children.
54+
var isDefault = !(props.path || props.name || props.children);
6155

62-
Path.extractParamNames(_parentRoute.props.path).forEach(function (paramName) {
56+
if (props.path || props.name) {
57+
props.path = Path.normalize(props.path || props.name);
58+
} else if (_parentRoute && _parentRoute.props.path) {
59+
props.path = _parentRoute.props.path;
60+
} else {
61+
props.path = '/';
62+
}
63+
64+
props.paramNames = Path.extractParamNames(props.path);
65+
66+
// Make sure the route's path has all params its parent needs.
67+
if (_parentRoute && Array.isArray(_parentRoute.props.paramNames)) {
68+
_parentRoute.props.paramNames.forEach(function (paramName) {
6369
invariant(
64-
paramNames.indexOf(paramName) !== -1,
65-
'The nested route path "' + route.props.path + '" is missing the "' + paramName + '" ' +
66-
'parameter of its parent path "' + _parentRoute.props.path + '"'
70+
props.paramNames.indexOf(paramName) !== -1,
71+
'The nested route path "%s" is missing the "%s" parameter of its parent path "%s"',
72+
props.path, paramName, _parentRoute.props.path
6773
);
6874
});
6975
}
7076

71-
// Make sure the <Route> can be looked up by <Link>s.
72-
if (route.props.name) {
73-
var existingRoute = _namedRoutes[route.props.name];
77+
// Make sure the route can be looked up by <Link>s.
78+
if (props.name) {
79+
var existingRoute = _namedRoutes[props.name];
7480

7581
invariant(
7682
!existingRoute || route === existingRoute,
77-
'You cannot use the name "' + route.props.name + '" for more than one route'
83+
'You cannot use the name "%s" for more than one route',
84+
props.name
7885
);
7986

80-
_namedRoutes[route.props.name] = route;
87+
_namedRoutes[props.name] = route;
8188
}
8289

83-
React.Children.forEach(route.props.children, function (child) {
84-
RouteStore.registerRoute(child, route);
90+
if (_parentRoute && isDefault) {
91+
invariant(
92+
_parentRoute.props.defaultRoute == null,
93+
'You may not have more than one <DefaultRoute> per <Route>'
94+
);
95+
96+
_parentRoute.props.defaultRoute = route;
97+
98+
return null;
99+
}
100+
101+
// Make sure children is an array, excluding <DefaultRoute>s.
102+
var children = [];
103+
104+
React.Children.forEach(props.children, function (child) {
105+
if (child = RouteStore.registerRoute(child, route))
106+
children.push(child);
85107
});
108+
109+
props.children = children;
110+
111+
return route;
86112
},
87113

88114
/**

0 commit comments

Comments
 (0)