X Tutup
Skip to content

Commit 4ea5b6e

Browse files
committed
feat(router): implement router link DSL
Closes angular#5557 Closes angular#5562
1 parent e67e195 commit 4ea5b6e

File tree

8 files changed

+365
-8
lines changed

8 files changed

+365
-8
lines changed

gulpfile.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,10 +1117,22 @@ gulp.task('!bundles.js.umd', ['build.js.dev'], function() {
11171117
return q.all([
11181118
webpack(webPackConf(['angular2/angular2.js'], 'angular2', 'dev')),
11191119
webpack(webPackConf(['angular2/angular2.js'], 'angular2', 'prod')),
1120-
webpack(webPackConf(['angular2/angular2.js', 'angular2/http.js', 'angular2/router.js'],
1121-
'angular2_all', 'dev')),
1122-
webpack(webPackConf(['angular2/angular2.js', 'angular2/http.js', 'angular2/router.js'],
1123-
'angular2_all', 'prod'))
1120+
webpack(webPackConf(
1121+
[
1122+
'angular2/angular2.js',
1123+
'angular2/http.js',
1124+
'angular2/router/router_link_dsl.js',
1125+
'angular2/router.js'
1126+
],
1127+
'angular2_all', 'dev')),
1128+
webpack(webPackConf(
1129+
[
1130+
'angular2/angular2.js',
1131+
'angular2/http.js',
1132+
'angular2/router/router_link_dsl.js',
1133+
'angular2/router.js'
1134+
],
1135+
'angular2_all', 'prod'))
11241136
]);
11251137
});
11261138

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {TEMPLATE_TRANSFORMS} from 'angular2/compiler';
2+
import {Provider} from 'angular2/core';
3+
import {RouterLinkTransform} from 'angular2/src/router/router_link_transform';
4+
import {CONST_EXPR} from 'angular2/src/facade/lang';
5+
6+
export {RouterLinkTransform} from 'angular2/src/router/router_link_transform';
7+
8+
/**
9+
* Enables the router link DSL.
10+
*
11+
* Warning. This feature is experimental and can change.
12+
*
13+
* To enable the transformer pass the router link DSL provider to `bootstrap`.
14+
*
15+
* ## Example:
16+
* ```
17+
* import {bootstrap} from 'angular2/platform/browser';
18+
* import {ROUTER_LINK_DSL_PROVIDER} from 'angular2/router/router_link_dsl';
19+
*
20+
* bootstrap(CustomApp, [ROUTER_LINK_DSL_PROVIDER]);
21+
* ```
22+
*
23+
* The DSL allows you to express router links as follows:
24+
* ```
25+
* <a [routerLink]="route:User"> <!-- Same as <a [routerLink]="['User']"> -->
26+
* <a [routerLink]="route:/User"> <!-- Same as <a [routerLink]="['User']"> -->
27+
* <a [routerLink]="route:./User"> <!-- Same as <a [routerLink]="['./User']"> -->
28+
* <a [routerLink]="./User(id: value, name: 'Bob')"> <!-- Same as <a [routerLink]="['./User', {id:
29+
* value, name: 'Bob'}]"> -->
30+
* <a [routerLink]="/User/Modal"> <!-- Same as <a [routerLink]="['/User', 'Modal']"> -->
31+
* <a [routerLink]="User[Modal]"> <!-- Same as <a [routerLink]="['User', ['Modal']]"> -->
32+
* ```
33+
*/
34+
const ROUTER_LINK_DSL_PROVIDER =
35+
CONST_EXPR(new Provider(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true}));

modules/angular2/src/compiler/compiler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export {
77
} from './directive_metadata';
88
export {SourceModule, SourceWithImports} from './source_module';
99
export {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';
10-
10+
export * from 'angular2/src/compiler/template_ast';
11+
export {TEMPLATE_TRANSFORMS} from 'angular2/src/compiler/template_parser';
1112
import {assertionsEnabled, Type, CONST_EXPR} from 'angular2/src/facade/lang';
1213
import {provide, Provider} from 'angular2/src/core/di';
1314
import {TemplateParser} from 'angular2/src/compiler/template_parser';
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import {
2+
TemplateAstVisitor,
3+
ElementAst,
4+
BoundDirectivePropertyAst,
5+
DirectiveAst,
6+
BoundElementPropertyAst
7+
} from 'angular2/compiler';
8+
import {
9+
AstTransformer,
10+
Quote,
11+
AST,
12+
EmptyExpr,
13+
LiteralArray,
14+
LiteralPrimitive,
15+
ASTWithSource
16+
} from 'angular2/src/core/change_detection/parser/ast';
17+
import {BaseException} from 'angular2/src/facade/exceptions';
18+
import {Injectable} from 'angular2/core';
19+
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
20+
21+
/**
22+
* e.g., './User', 'Modal' in ./User[Modal(param: value)]
23+
*/
24+
class FixedPart {
25+
constructor(public value: string) {}
26+
}
27+
28+
/**
29+
* The square bracket
30+
*/
31+
class AuxiliaryStart {
32+
constructor() {}
33+
}
34+
35+
/**
36+
* The square bracket
37+
*/
38+
class AuxiliaryEnd {
39+
constructor() {}
40+
}
41+
42+
/**
43+
* e.g., param:value in ./User[Modal(param: value)]
44+
*/
45+
class Params {
46+
constructor(public ast: AST) {}
47+
}
48+
49+
class RouterLinkLexer {
50+
index: number = 0;
51+
52+
constructor(private parser: Parser, private exp: string) {}
53+
54+
tokenize(): Array<FixedPart | AuxiliaryStart | AuxiliaryEnd | Params> {
55+
let tokens = [];
56+
while (this.index < this.exp.length) {
57+
tokens.push(this._parseToken());
58+
}
59+
return tokens;
60+
}
61+
62+
private _parseToken() {
63+
let c = this.exp[this.index];
64+
if (c == '[') {
65+
this.index++;
66+
return new AuxiliaryStart();
67+
68+
} else if (c == ']') {
69+
this.index++;
70+
return new AuxiliaryEnd();
71+
72+
} else if (c == '(') {
73+
return this._parseParams();
74+
75+
} else if (c == '/' && this.index !== 0) {
76+
this.index++;
77+
return this._parseFixedPart();
78+
79+
} else {
80+
return this._parseFixedPart();
81+
}
82+
}
83+
84+
private _parseParams() {
85+
let start = this.index;
86+
for (; this.index < this.exp.length; ++this.index) {
87+
let c = this.exp[this.index];
88+
if (c == ')') {
89+
let paramsContent = this.exp.substring(start + 1, this.index);
90+
this.index++;
91+
return new Params(this.parser.parseBinding(`{${paramsContent}}`, null).ast);
92+
}
93+
}
94+
throw new BaseException("Cannot find ')'");
95+
}
96+
97+
private _parseFixedPart() {
98+
let start = this.index;
99+
let sawNonSlash = false;
100+
101+
102+
for (; this.index < this.exp.length; ++this.index) {
103+
let c = this.exp[this.index];
104+
105+
if (c == '(' || c == '[' || c == ']' || (c == '/' && sawNonSlash)) {
106+
break;
107+
}
108+
109+
if (c != '.' && c != '/') {
110+
sawNonSlash = true;
111+
}
112+
}
113+
114+
let fixed = this.exp.substring(start, this.index);
115+
116+
if (start === this.index || !sawNonSlash || fixed.startsWith('//')) {
117+
throw new BaseException("Invalid router link");
118+
}
119+
120+
return new FixedPart(fixed);
121+
}
122+
}
123+
124+
class RouterLinkAstGenerator {
125+
index: number = 0;
126+
constructor(private tokens: any[]) {}
127+
128+
generate(): AST { return this._genAuxiliary(); }
129+
130+
private _genAuxiliary(): AST {
131+
let arr = [];
132+
for (; this.index < this.tokens.length; this.index++) {
133+
let r = this.tokens[this.index];
134+
135+
if (r instanceof FixedPart) {
136+
arr.push(new LiteralPrimitive(r.value));
137+
138+
} else if (r instanceof Params) {
139+
arr.push(r.ast);
140+
141+
} else if (r instanceof AuxiliaryEnd) {
142+
break;
143+
144+
} else if (r instanceof AuxiliaryStart) {
145+
this.index++;
146+
arr.push(this._genAuxiliary());
147+
}
148+
}
149+
150+
return new LiteralArray(arr);
151+
}
152+
}
153+
154+
class RouterLinkAstTransformer extends AstTransformer {
155+
constructor(private parser: Parser) { super(); }
156+
157+
visitQuote(ast: Quote): AST {
158+
if (ast.prefix == "route") {
159+
return parseRouterLinkExpression(this.parser, ast.uninterpretedExpression);
160+
} else {
161+
return super.visitQuote(ast);
162+
}
163+
}
164+
}
165+
166+
export function parseRouterLinkExpression(parser: Parser, exp: string): AST {
167+
let tokens = new RouterLinkLexer(parser, exp.trim()).tokenize();
168+
return new RouterLinkAstGenerator(tokens).generate();
169+
}
170+
171+
/**
172+
* A compiler plugin that implements the router link DSL.
173+
*/
174+
@Injectable()
175+
export class RouterLinkTransform implements TemplateAstVisitor {
176+
private astTransformer;
177+
178+
constructor(parser: Parser) { this.astTransformer = new RouterLinkAstTransformer(parser); }
179+
180+
visitNgContent(ast: any, context: any): any { return ast; }
181+
182+
visitEmbeddedTemplate(ast: any, context: any): any { return ast; }
183+
184+
visitElement(ast: ElementAst, context: any): any {
185+
let updatedChildren = ast.children.map(c => c.visit(this, context));
186+
let updatedInputs = ast.inputs.map(c => c.visit(this, context));
187+
let updatedDirectives = ast.directives.map(c => c.visit(this, context));
188+
return new ElementAst(ast.name, ast.attrs, updatedInputs, ast.outputs, ast.exportAsVars,
189+
updatedDirectives, updatedChildren, ast.ngContentIndex, ast.sourceSpan);
190+
}
191+
192+
visitVariable(ast: any, context: any): any { return ast; }
193+
194+
visitEvent(ast: any, context: any): any { return ast; }
195+
196+
visitElementProperty(ast: any, context: any): any { return ast; }
197+
198+
visitAttr(ast: any, context: any): any { return ast; }
199+
200+
visitBoundText(ast: any, context: any): any { return ast; }
201+
202+
visitText(ast: any, context: any): any { return ast; }
203+
204+
visitDirective(ast: DirectiveAst, context: any): any {
205+
let updatedInputs = ast.inputs.map(c => c.visit(this, context));
206+
return new DirectiveAst(ast.directive, updatedInputs, ast.hostProperties, ast.hostEvents,
207+
ast.exportAsVars, ast.sourceSpan);
208+
}
209+
210+
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
211+
let transformedValue = ast.value.visit(this.astTransformer);
212+
return new BoundDirectivePropertyAst(ast.directiveName, ast.templateName, transformedValue,
213+
ast.sourceSpan);
214+
}
215+
}

modules/angular2/test/router/integration/router_link_spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
import {RootRouter} from 'angular2/src/router/router';
4242

4343
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
44+
import {TEMPLATE_TRANSFORMS} from 'angular2/compiler';
45+
import {RouterLinkTransform} from 'angular2/src/router/router_link_transform';
4446

4547
export function main() {
4648
describe('router-link directive', function() {
@@ -53,7 +55,8 @@ export function main() {
5355
DirectiveResolver,
5456
provide(Location, {useClass: SpyLocation}),
5557
provide(ROUTER_PRIMARY_COMPONENT, {useValue: MyComp}),
56-
provide(Router, {useClass: RootRouter})
58+
provide(Router, {useClass: RootRouter}),
59+
provide(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true})
5760
]);
5861

5962
beforeEach(inject([TestComponentBuilder, Router, Location], (tcBuilder, rtr, loc) => {
@@ -320,6 +323,25 @@ export function main() {
320323
router.navigateByUrl('/child-with-grandchild/grandchild');
321324
});
322325
}));
326+
327+
328+
describe("router link dsl", () => {
329+
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
330+
compile('<a href="hello" [router-link]="route:./User(name: name)">{{name}}</a>')
331+
.then((_) => router.config(
332+
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
333+
.then((_) => router.navigateByUrl('/a/b'))
334+
.then((_) => {
335+
fixture.debugElement.componentInstance.name = 'brian';
336+
fixture.detectChanges();
337+
expect(fixture.debugElement.nativeElement).toHaveText('brian');
338+
expect(DOM.getAttribute(
339+
fixture.debugElement.componentViewChildren[0].nativeElement, 'href'))
340+
.toEqual('/user/brian');
341+
async.done();
342+
});
343+
}));
344+
});
323345
});
324346

325347
describe('when clicked', () => {
@@ -360,6 +382,7 @@ export function main() {
360382
.then((_) => {
361383
fixture.detectChanges();
362384

385+
363386
var dispatchedEvent = clickOnElement(fixture);
364387
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);
365388

0 commit comments

Comments
 (0)
X Tutup