Skip to content

Commit 6ddfff5

Browse files
committed
refactor(router): improve recognition and generation pipeline
This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes angular#4728 Closes angular#4228 Closes angular#4170 Closes angular#4490 Closes angular#4694 Closes angular#5200 Closes angular#5475
1 parent a325321 commit 6ddfff5

43 files changed

Lines changed: 3082 additions & 1166 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/angular1_router/build.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ var ts = require('typescript');
66
var files = [
77
'lifecycle_annotations_impl.ts',
88
'url_parser.ts',
9-
'path_recognizer.ts',
9+
'route_recognizer.ts',
1010
'route_config_impl.ts',
1111
'async_route_handler.ts',
1212
'sync_route_handler.ts',
13-
'route_recognizer.ts',
13+
'component_recognizer.ts',
1414
'instruction.ts',
15+
'path_recognizer.ts',
1516
'route_config_nomalizer.ts',
1617
'route_lifecycle_reflector.ts',
1718
'route_registry.ts',
@@ -39,7 +40,10 @@ function main() {
3940
* sourcemap, and exported variable identifier name for the content.
4041
*/
4142
var IMPORT_RE = new RegExp("import \\{?([\\w\\n_, ]+)\\}? from '(.+)';?", 'g');
43+
var INJECT_RE = new RegExp("@Inject\\(ROUTER_PRIMARY_COMPONENT\\)", 'g');
44+
var IMJECTABLE_RE = new RegExp("@Injectable\\(\\)", 'g');
4245
function transform(contents) {
46+
contents = contents.replace(INJECT_RE, '').replace(IMJECTABLE_RE, '');
4347
contents = contents.replace(IMPORT_RE, function (match, imports, includePath) {
4448
//TODO: remove special-case
4549
if (isFacadeModule(includePath) || includePath === './router_outlet') {

modules/angular1_router/lib/facades.es5

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ var StringMapWrapper = {
173173

174174
var List = Array;
175175
var ListWrapper = {
176+
clear: function (l) {
177+
l.length = 0;
178+
},
179+
176180
create: function () {
177181
return [];
178182
},

modules/angular1_router/src/module_template.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
99
// the contents of `../lib/facades.es5`.
1010
//{{FACADES}}
1111

12-
var exports = {Injectable: function () {}};
12+
var exports = {
13+
Injectable: function () {},
14+
OpaqueToken: function () {},
15+
Inject: function () {}
16+
};
1317
var require = function () {return exports;};
1418

1519
// When this file is processed, the line below is replaced with
@@ -31,12 +35,19 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
3135
// property in a route config
3236
exports.assertComponentExists = function () {};
3337

34-
angular.stringifyInstruction = exports.stringifyInstruction;
38+
angular.stringifyInstruction = function (instruction) {
39+
return instruction.toRootUrl();
40+
};
3541

3642
var RouteRegistry = exports.RouteRegistry;
3743
var RootRouter = exports.RootRouter;
3844

39-
var registry = new RouteRegistry();
45+
46+
// Because Angular 1 has no notion of a root component, we use an object with unique identity
47+
// to represent this.
48+
var ROOT_COMPONENT_OBJECT = new Object();
49+
50+
var registry = new RouteRegistry(ROOT_COMPONENT_OBJECT);
4051
var location = new Location();
4152

4253
$$directiveIntrospector(function (name, factory) {
@@ -47,10 +58,6 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
4758
}
4859
});
4960

50-
// Because Angular 1 has no notion of a root component, we use an object with unique identity
51-
// to represent this.
52-
var ROOT_COMPONENT_OBJECT = new Object();
53-
5461
var router = new RootRouter(registry, location, ROOT_COMPONENT_OBJECT);
5562
$rootScope.$watch(function () { return $location.path(); }, function (path) {
5663
if (router.lastNavigationAttempt !== path) {

modules/angular1_router/src/ng_route_shim.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
routeMap[path] = routeCopy;
111111

112112
if (route.redirectTo) {
113-
routeDefinition.redirectTo = route.redirectTo;
113+
routeDefinition.redirectTo = [routeMap[route.redirectTo].name];
114114
} else {
115115
if (routeCopy.controller && !routeCopy.controllerAs) {
116116
console.warn('Route for "' + path + '" should use "controllerAs".');
@@ -123,7 +123,7 @@
123123
}
124124

125125
routeDefinition.component = directiveName;
126-
routeDefinition.as = upperCase(directiveName);
126+
routeDefinition.name = route.name || upperCase(directiveName);
127127

128128
var directiveController = routeCopy.controller;
129129

modules/angular1_router/test/integration/navigation_spec.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,7 @@ describe('navigation', function () {
113113
});
114114

115115

116-
// TODO: fix this
117-
xit('should work with recursive nested outlets', function () {
116+
it('should work with recursive nested outlets', function () {
118117
registerComponent('recurCmp', {
119118
template: '<div>recur { <div ng-outlet></div> }</div>',
120119
$routeConfig: [
@@ -152,8 +151,8 @@ describe('navigation', function () {
152151
compile('<div ng-outlet></div>');
153152

154153
$router.config([
155-
{ path: '/', redirectTo: '/user' },
156-
{ path: '/user', component: 'userCmp' }
154+
{ path: '/', redirectTo: ['/User'] },
155+
{ path: '/user', component: 'userCmp', name: 'User' }
157156
]);
158157

159158
$router.navigateByUrl('/');
@@ -167,16 +166,15 @@ describe('navigation', function () {
167166
registerComponent('childRouter', {
168167
template: '<div>inner { <div ng-outlet></div> }</div>',
169168
$routeConfig: [
170-
{ path: '/old-child', redirectTo: '/new-child' },
171-
{ path: '/new-child', component: 'oneCmp'},
172-
{ path: '/old-child-two', redirectTo: '/new-child-two' },
173-
{ path: '/new-child-two', component: 'twoCmp'}
169+
{ path: '/new-child', component: 'oneCmp', name: 'NewChild'},
170+
{ path: '/new-child-two', component: 'twoCmp', name: 'NewChildTwo'}
174171
]
175172
});
176173

177174
$router.config([
178-
{ path: '/old-parent', redirectTo: '/new-parent' },
179-
{ path: '/new-parent/...', component: 'childRouter' }
175+
{ path: '/old-parent/old-child', redirectTo: ['/NewParent', 'NewChild'] },
176+
{ path: '/old-parent/old-child-two', redirectTo: ['/NewParent', 'NewChildTwo'] },
177+
{ path: '/new-parent/...', component: 'childRouter', name: 'NewParent' }
180178
]);
181179

182180
compile('<div ng-outlet></div>');

modules/angular1_router/test/integration/shim_spec.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,12 @@ describe('ngRoute shim', function () {
139139

140140
it('should adapt routes with redirects', inject(function ($location) {
141141
$routeProvider
142+
.when('/home', {
143+
template: 'welcome home!',
144+
name: 'Home'
145+
})
142146
.when('/', {
143147
redirectTo: '/home'
144-
})
145-
.when('/home', {
146-
template: 'welcome home!'
147148
});
148149
$rootScope.$digest();
149150

modules/angular2/router.ts

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export {Router} from './src/router/router';
88
export {RouterOutlet} from './src/router/router_outlet';
99
export {RouterLink} from './src/router/router_link';
1010
export {RouteParams, RouteData} from './src/router/instruction';
11-
export {RouteRegistry} from './src/router/route_registry';
1211
export {PlatformLocation} from './src/router/platform_location';
12+
export {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './src/router/route_registry';
1313
export {LocationStrategy, APP_BASE_HREF} from './src/router/location_strategy';
1414
export {HashLocationStrategy} from './src/router/hash_location_strategy';
1515
export {PathLocationStrategy} from './src/router/path_location_strategy';
@@ -27,41 +27,12 @@ import {PathLocationStrategy} from './src/router/path_location_strategy';
2727
import {Router, RootRouter} from './src/router/router';
2828
import {RouterOutlet} from './src/router/router_outlet';
2929
import {RouterLink} from './src/router/router_link';
30-
import {RouteRegistry} from './src/router/route_registry';
30+
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './src/router/route_registry';
3131
import {Location} from './src/router/location';
3232
import {ApplicationRef, provide, OpaqueToken, Provider} from 'angular2/core';
3333
import {CONST_EXPR} from './src/facade/lang';
3434
import {BaseException} from 'angular2/src/facade/exceptions';
3535

36-
37-
/**
38-
* Token used to bind the component with the top-level {@link RouteConfig}s for the
39-
* application.
40-
*
41-
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
42-
*
43-
* ```
44-
* import {Component} from 'angular2/angular2';
45-
* import {
46-
* ROUTER_DIRECTIVES,
47-
* ROUTER_PROVIDERS,
48-
* RouteConfig
49-
* } from 'angular2/router';
50-
*
51-
* @Component({directives: [ROUTER_DIRECTIVES]})
52-
* @RouteConfig([
53-
* {...},
54-
* ])
55-
* class AppCmp {
56-
* // ...
57-
* }
58-
*
59-
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
60-
* ```
61-
*/
62-
export const ROUTER_PRIMARY_COMPONENT: OpaqueToken =
63-
CONST_EXPR(new OpaqueToken('RouterPrimaryComponent'));
64-
6536
/**
6637
* A list of directives. To use the router directives like {@link RouterOutlet} and
6738
* {@link RouterLink}, add this to your `directives` array in the {@link View} decorator of your

modules/angular2/src/router/async_route_handler.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import {RouteHandler} from './route_handler';
21
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
32
import {isPresent, Type} from 'angular2/src/facade/lang';
43

4+
import {RouteHandler} from './route_handler';
5+
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
6+
7+
58
export class AsyncRouteHandler implements RouteHandler {
69
/** @internal */
710
_resolvedComponent: Promise<any> = null;
811
componentType: Type;
12+
public data: RouteData;
913

10-
constructor(private _loader: Function, public data?: {[key: string]: any}) {}
14+
constructor(private _loader: Function, data: {[key: string]: any} = null) {
15+
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
16+
}
1117

1218
resolveComponentType(): Promise<any> {
1319
if (isPresent(this._resolvedComponent)) {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
2+
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
3+
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
4+
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
5+
6+
import {
7+
AbstractRecognizer,
8+
RouteRecognizer,
9+
RedirectRecognizer,
10+
RouteMatch
11+
} from './route_recognizer';
12+
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
13+
import {AsyncRouteHandler} from './async_route_handler';
14+
import {SyncRouteHandler} from './sync_route_handler';
15+
import {Url} from './url_parser';
16+
import {ComponentInstruction} from './instruction';
17+
18+
19+
/**
20+
* `ComponentRecognizer` is responsible for recognizing routes for a single component.
21+
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
22+
* components.
23+
*/
24+
export class ComponentRecognizer {
25+
names = new Map<string, RouteRecognizer>();
26+
27+
// map from name to recognizer
28+
auxNames = new Map<string, RouteRecognizer>();
29+
30+
// map from starting path to recognizer
31+
auxRoutes = new Map<string, RouteRecognizer>();
32+
33+
// TODO: optimize this into a trie
34+
matchers: AbstractRecognizer[] = [];
35+
36+
defaultRoute: RouteRecognizer = null;
37+
38+
/**
39+
* returns whether or not the config is terminal
40+
*/
41+
config(config: RouteDefinition): boolean {
42+
var handler;
43+
44+
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
45+
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
46+
throw new BaseException(
47+
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
48+
}
49+
50+
if (config instanceof AuxRoute) {
51+
handler = new SyncRouteHandler(config.component, config.data);
52+
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
53+
var recognizer = new RouteRecognizer(config.path, handler);
54+
this.auxRoutes.set(path, recognizer);
55+
if (isPresent(config.name)) {
56+
this.auxNames.set(config.name, recognizer);
57+
}
58+
return recognizer.terminal;
59+
}
60+
61+
var useAsDefault = false;
62+
63+
if (config instanceof Redirect) {
64+
let redirector = new RedirectRecognizer(config.path, config.redirectTo);
65+
this._assertNoHashCollision(redirector.hash, config.path);
66+
this.matchers.push(redirector);
67+
return true;
68+
}
69+
70+
if (config instanceof Route) {
71+
handler = new SyncRouteHandler(config.component, config.data);
72+
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
73+
} else if (config instanceof AsyncRoute) {
74+
handler = new AsyncRouteHandler(config.loader, config.data);
75+
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
76+
}
77+
var recognizer = new RouteRecognizer(config.path, handler);
78+
79+
this._assertNoHashCollision(recognizer.hash, config.path);
80+
81+
if (useAsDefault) {
82+
if (isPresent(this.defaultRoute)) {
83+
throw new BaseException(`Only one route can be default`);
84+
}
85+
this.defaultRoute = recognizer;
86+
}
87+
88+
this.matchers.push(recognizer);
89+
if (isPresent(config.name)) {
90+
this.names.set(config.name, recognizer);
91+
}
92+
return recognizer.terminal;
93+
}
94+
95+
96+
private _assertNoHashCollision(hash: string, path) {
97+
this.matchers.forEach((matcher) => {
98+
if (hash == matcher.hash) {
99+
throw new BaseException(
100+
`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
101+
}
102+
});
103+
}
104+
105+
106+
/**
107+
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
108+
*/
109+
recognize(urlParse: Url): Promise<RouteMatch>[] {
110+
var solutions = [];
111+
112+
this.matchers.forEach((routeRecognizer: AbstractRecognizer) => {
113+
var pathMatch = routeRecognizer.recognize(urlParse);
114+
115+
if (isPresent(pathMatch)) {
116+
solutions.push(pathMatch);
117+
}
118+
});
119+
120+
return solutions;
121+
}
122+
123+
recognizeAuxiliary(urlParse: Url): Promise<RouteMatch>[] {
124+
var routeRecognizer: RouteRecognizer = this.auxRoutes.get(urlParse.path);
125+
if (isPresent(routeRecognizer)) {
126+
return [routeRecognizer.recognize(urlParse)];
127+
}
128+
129+
return [PromiseWrapper.resolve(null)];
130+
}
131+
132+
hasRoute(name: string): boolean { return this.names.has(name); }
133+
134+
componentLoaded(name: string): boolean {
135+
return this.hasRoute(name) && isPresent(this.names.get(name).handler.componentType);
136+
}
137+
138+
loadComponent(name: string): Promise<any> {
139+
return this.names.get(name).handler.resolveComponentType();
140+
}
141+
142+
generate(name: string, params: any): ComponentInstruction {
143+
var pathRecognizer: RouteRecognizer = this.names.get(name);
144+
if (isBlank(pathRecognizer)) {
145+
return null;
146+
}
147+
return pathRecognizer.generate(params);
148+
}
149+
150+
generateAuxiliary(name: string, params: any): ComponentInstruction {
151+
var pathRecognizer: RouteRecognizer = this.auxNames.get(name);
152+
if (isBlank(pathRecognizer)) {
153+
return null;
154+
}
155+
return pathRecognizer.generate(params);
156+
}
157+
}

0 commit comments

Comments
 (0)