Skip to content

Commit c8a29a6

Browse files
committed
Merge pull request #845 from rackt/add-route
Improved core primitives, programmatic configuration
2 parents c64e2e0 + 1bb6f1d commit c8a29a6

File tree

11 files changed

+496
-279
lines changed

11 files changed

+496
-279
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)