Skip to content

Commit c5a24a5

Browse files
committed
[added] Route/Match classes
[changed] Route names are nested This commit formalizes and enhances two of the core primitives in the router: Route and Match. We get a few benefits from this: 1. Routes may now be created programmatically, as well as via JSX. This is useful in situations where it is desirable to assemble the route configuration using separate modules, instead of all at once. For example, in ApplicationRoute.js you could have: module.exports = Router.createRoute(); and in UserProfileRoute.js: var ApplicationRoute = require('./ApplicationRoute'); module.exports = Router.createRoute({ parentRoute: ApplicationRoute, path: 'users/:id' }); 2. <Link to> may reference a Route object directly. <Link to={UserProfileRoute}> 3. Route names may be re-used at different levels of the hierarchy. For example, you could have two different routes named "new" but nested inside different parent routes. <Route name="users" handler={Users}> <DefaultRoute handler={ShowAllUsers}/> <Route name="new" handler={NewUser}/> </Route> <Route name="posts" handler={Posts}> <DefaultRoute handler={ShowAllPosts}/> <Route name="new" handler={NewPost}/> </Route> Using this route configuration, you could <Link to="users.new"> or <Link to="posts.new"> depending on which one you wanted. A side effect of this is that names of nested routes are no longer "global", so e.g. <Link to="new"> won't work because it is ambiguous, but <Link to="posts"> will still work.
1 parent 4a14a43 commit c5a24a5

File tree

9 files changed

+495
-251
lines changed

9 files changed

+495
-251
lines changed

modules/Match.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* jshint -W084 */
2+
3+
var Path = require('./utils/Path');
4+
5+
function Match(pathname, params, query, routes) {
6+
this.pathname = pathname;
7+
this.params = params;
8+
this.query = query;
9+
this.routes = routes;
10+
}
11+
12+
function deepSearch(route, pathname, query) {
13+
// Check the subtree first to find the most deeply-nested match.
14+
var childRoutes = route.childRoutes;
15+
if (childRoutes) {
16+
var match, childRoute;
17+
for (var i = 0, len = childRoutes.length; i < len; ++i) {
18+
childRoute = childRoutes[i];
19+
20+
if (childRoute.isDefault || childRoute.isNotFound)
21+
continue; // Check these in order later.
22+
23+
if (match = deepSearch(childRoute, pathname, query)) {
24+
// A route in the subtree matched! Add this route and we're done.
25+
match.routes.unshift(route);
26+
return match;
27+
}
28+
}
29+
}
30+
31+
// No child routes matched; try the default route.
32+
var defaultRoute = route.defaultRoute;
33+
if (defaultRoute && (params = Path.extractParams(defaultRoute.path, pathname)))
34+
return new Match(pathname, params, query, [ route, defaultRoute ]);
35+
36+
// Does the "not found" route match?
37+
var notFoundRoute = route.notFoundRoute;
38+
if (notFoundRoute && (params = Path.extractParams(notFoundRoute.path, pathname)))
39+
return new Match(pathname, params, query, [ route, notFoundRoute ]);
40+
41+
// Last attempt: check this route.
42+
var params = Path.extractParams(route.path, pathname);
43+
if (params)
44+
return new Match(pathname, params, query, [ route ]);
45+
46+
return null;
47+
}
48+
49+
/**
50+
* Attempts to match depth-first a route in the given route's
51+
* subtree against the given path and returns the match if it
52+
* succeeds, null if no match can be made.
53+
*/
54+
Match.findMatchForPath = function (routes, path) {
55+
var pathname = Path.withoutQuery(path);
56+
var query = Path.extractQuery(path);
57+
var match = null;
58+
59+
for (var i = 0, len = routes.length; match == null && i < len; ++i)
60+
match = deepSearch(routes[i], pathname, query);
61+
62+
return match;
63+
};
64+
65+
module.exports = Match;

modules/Route.js

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
var assign = require('react/lib/Object.assign');
2+
var invariant = require('react/lib/invariant');
3+
var warning = require('react/lib/warning');
4+
var Path = require('./utils/Path');
5+
6+
function Route(name, path, ignoreScrollBehavior, isDefault, isNotFound, onEnter, onLeave, handler) {
7+
this.name = name;
8+
this.path = path;
9+
this.paramNames = Path.extractParamNames(this.path);
10+
this.ignoreScrollBehavior = !!ignoreScrollBehavior;
11+
this.isDefault = !!isDefault;
12+
this.isNotFound = !!isNotFound;
13+
this.onEnter = onEnter;
14+
this.onLeave = onLeave;
15+
this.handler = handler;
16+
}
17+
18+
Route.prototype.toString = function () {
19+
var string = '<Route';
20+
21+
if (this.name)
22+
string += ` name="${this.name}"`;
23+
24+
string += ` path="${this.path}">`;
25+
26+
return string;
27+
};
28+
29+
/**
30+
* Appends the given route to this route's child routes.
31+
*/
32+
Route.prototype.appendChildRoute = function (route) {
33+
invariant(
34+
route instanceof Route,
35+
'route.appendChildRoute must use a valid Route'
36+
);
37+
38+
if (!this.childRoutes)
39+
this.childRoutes = [];
40+
41+
if (route.name) {
42+
invariant(
43+
this.childRoutes.every(function (childRoute) {
44+
return childRoute.name !== route.name;
45+
}),
46+
'Route %s may not have more than one child route named "%s"',
47+
this, route.name
48+
);
49+
}
50+
51+
this.childRoutes.push(route);
52+
};
53+
54+
/**
55+
* Allows looking up a child route using a "." delimited string, e.g.:
56+
*
57+
* route.appendChildRoute(
58+
* Router.createRoute({ name: 'user' }, function () {
59+
* Router.createRoute({ name: 'new' });
60+
* })
61+
* );
62+
*
63+
* var NewUserRoute = route.lookupChildRoute('user.new');
64+
*
65+
* See also Route.findRouteByName.
66+
*/
67+
Route.prototype.lookupChildRoute = function (names) {
68+
if (!this.childRoutes)
69+
return null;
70+
71+
return Route.findRouteByName(this.childRoutes, names);
72+
};
73+
74+
/**
75+
* Searches the given array of routes and returns the route that matches
76+
* the given name. The name should be a . delimited string like "user.new"
77+
* that specifies the names of nested routes. Routes in the hierarchy that
78+
* do not have a name do not need to be specified in the search string.
79+
*
80+
* var routes = [
81+
* Router.createRoute({ name: 'user' }, function () {
82+
* Router.createRoute({ name: 'new' });
83+
* })
84+
* ];
85+
*
86+
* var NewUserRoute = Route.findRouteByName(routes, 'user.new');
87+
*/
88+
Route.findRouteByName = function (routes, names) {
89+
if (typeof names === 'string')
90+
names = names.split('.');
91+
92+
var route, foundRoute;
93+
for (var i = 0, len = routes.length; i < len; ++i) {
94+
route = routes[i];
95+
96+
if (route.name === names[0]) {
97+
if (names.length === 1)
98+
return route;
99+
100+
if (!route.childRoutes)
101+
return null;
102+
103+
return Route.findRouteByName(route.childRoutes, names.slice(1));
104+
} else if (route.name == null) {
105+
// Transparently skip over unnamed routes in the tree.
106+
foundRoute = route.lookupChildRoute(names);
107+
108+
if (foundRoute != null)
109+
return foundRoute;
110+
}
111+
}
112+
113+
return null;
114+
};
115+
116+
var _currentRoute;
117+
118+
/**
119+
* Creates and returns a new route. Options may be a URL pathname string
120+
* with placeholders for named params or an object with any of the following
121+
* properties:
122+
*
123+
* - name The name of the route. This is used to lookup a
124+
* route relative to its parent route and should be
125+
* unique among all child routes of the same parent
126+
* - path A URL pathname string with optional placeholders
127+
* that specify the names of params to extract from
128+
* the URL when the path matches. Defaults to `/${name}`
129+
* when there is a name given, or the path of the parent
130+
* route, or /
131+
* - ignoreScrollBehavior True to make this route (and all descendants) ignore
132+
* the scroll behavior of the router
133+
* - isDefault True to make this route the default route among all
134+
* its siblings
135+
* - isNotFound True to make this route the "not found" route among
136+
* all its siblings
137+
* - onEnter A transition hook that will be called when the
138+
* router is going to enter this route
139+
* - onLeave A transition hook that will be called when the
140+
* router is going to leave this route
141+
* - handler A React component that will be rendered when
142+
* this route is active
143+
* - parentRoute The parent route to use for this route. This option
144+
* is automatically supplied when creating routes inside
145+
* the callback to another invocation of createRoute. You
146+
* only ever need to use this when declaring routes
147+
* independently of one another to manually piece together
148+
* the route hierarchy
149+
*
150+
* The callback may be used to structure your route hierarchy. Any call to
151+
* createRoute, createDefaultRoute, createNotFoundRoute, or createRedirect
152+
* inside the callback automatically uses this route as its parent.
153+
*/
154+
Route.createRoute = function (options, callback) {
155+
options = options || {};
156+
157+
if (typeof options === 'string')
158+
options = { path: options };
159+
160+
var parentRoute = _currentRoute;
161+
162+
if (parentRoute) {
163+
warning(
164+
options.parentRoute == null || options.parentRoute === parentRoute,
165+
'You should not use parentRoute with createRoute inside another route\'s child callback; it is ignored'
166+
);
167+
} else {
168+
parentRoute = options.parentRoute;
169+
}
170+
171+
var name = options.name;
172+
var path = options.path || name;
173+
174+
if (path) {
175+
if (Path.isAbsolute(path)) {
176+
if (parentRoute) {
177+
invariant(
178+
parentRoute.paramNames.length === 0,
179+
'You cannot nest path "%s" inside "%s"; the parent requires URL parameters',
180+
path, parentRoute.path
181+
);
182+
}
183+
} else if (parentRoute) {
184+
// Relative paths extend their parent.
185+
path = Path.join(parentRoute.path, path);
186+
} else {
187+
path = '/' + path;
188+
}
189+
} else {
190+
path = parentRoute ? parentRoute.path : '/';
191+
}
192+
193+
if (options.isNotFound && !(/\*$/).test(path))
194+
path += '*'; // Auto-append * to the path of not found routes.
195+
196+
var route = new Route(
197+
name,
198+
path,
199+
options.ignoreScrollBehavior,
200+
options.isDefault,
201+
options.isNotFound,
202+
options.onEnter,
203+
options.onLeave,
204+
options.handler
205+
);
206+
207+
if (parentRoute) {
208+
if (route.isDefault) {
209+
invariant(
210+
parentRoute.defaultRoute == null,
211+
'%s may not have more than one default route',
212+
parentRoute
213+
);
214+
215+
parentRoute.defaultRoute = route;
216+
} else if (route.isNotFound) {
217+
invariant(
218+
parentRoute.notFoundRoute == null,
219+
'%s may not have more than one not found route',
220+
parentRoute
221+
);
222+
223+
parentRoute.notFoundRoute = route;
224+
}
225+
226+
parentRoute.appendChildRoute(route);
227+
}
228+
229+
// Any routes created in the callback
230+
// use this route as their parent.
231+
if (typeof callback === 'function') {
232+
var currentRoute = _currentRoute;
233+
_currentRoute = route;
234+
callback.call(route, route);
235+
_currentRoute = currentRoute;
236+
}
237+
238+
return route;
239+
};
240+
241+
/**
242+
* Creates and returns a route that is rendered when its parent matches
243+
* the current URL.
244+
*/
245+
Route.createDefaultRoute = function (options) {
246+
return Route.createRoute(
247+
assign({}, options, { isDefault: true })
248+
);
249+
};
250+
251+
/**
252+
* Creates and returns a route that is rendered when its parent matches
253+
* the current URL but none of its siblings do.
254+
*/
255+
Route.createNotFoundRoute = function (options) {
256+
return Route.createRoute(
257+
assign({}, options, { isNotFound: true })
258+
);
259+
};
260+
261+
/**
262+
* Creates and returns a route that automatically redirects the transition
263+
* to another route. In addition to the normal options to createRoute, this
264+
* function accepts the following options:
265+
*
266+
* - from An alias for the `path` option. Defaults to *
267+
* - to The path/route/route name to redirect to
268+
* - params The params to use in the redirect URL. Defaults
269+
* to using the current params
270+
* - query The query to use in the redirect URL. Defaults
271+
* to using the current query
272+
*/
273+
Route.createRedirect = function (options) {
274+
return Route.createRoute(
275+
assign({}, options, {
276+
path: options.path || options.from || '*',
277+
onEnter: function (transition, params, query) {
278+
transition.redirect(options.to, options.params || params, options.query || query);
279+
}
280+
})
281+
);
282+
};
283+
284+
module.exports = Route;

0 commit comments

Comments
 (0)