Skip to content

Commit cf7292f

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#4170 Closes angular#4490 Closes angular#4694 Closes angular#5200 Closes angular#5352
1 parent 422a7b1 commit cf7292f

41 files changed

Lines changed: 2943 additions & 1090 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: 3 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',

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
3131
// property in a route config
3232
exports.assertComponentExists = function () {};
3333

34-
angular.stringifyInstruction = exports.stringifyInstruction;
34+
angular.stringifyInstruction = function (instruction) {
35+
return instruction.toRootUrl();
36+
};
3537

3638
var RouteRegistry = exports.RouteRegistry;
3739
var RootRouter = exports.RootRouter;

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/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)