Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1117,10 +1117,22 @@ gulp.task('!bundles.js.umd', ['build.js.dev'], function() {
return q.all([
webpack(webPackConf(['angular2/angular2.js'], 'angular2', 'dev')),
webpack(webPackConf(['angular2/angular2.js'], 'angular2', 'prod')),
webpack(webPackConf(['angular2/angular2.js', 'angular2/http.js', 'angular2/router.js'],
'angular2_all', 'dev')),
webpack(webPackConf(['angular2/angular2.js', 'angular2/http.js', 'angular2/router.js'],
'angular2_all', 'prod'))
webpack(webPackConf(
[
'angular2/angular2.js',
'angular2/http.js',
'angular2/router/router_link_dsl.js',
'angular2/router.js'
],
'angular2_all', 'dev')),
webpack(webPackConf(
[
'angular2/angular2.js',
'angular2/http.js',
'angular2/router/router_link_dsl.js',
'angular2/router.js'
],
'angular2_all', 'prod'))
]);
});

Expand Down
35 changes: 35 additions & 0 deletions modules/angular2/router/router_link_dsl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {TEMPLATE_TRANSFORMS} from 'angular2/compiler';
import {Provider} from 'angular2/core';
import {RouterLinkTransform} from 'angular2/src/router/router_link_transform';
import {CONST_EXPR} from 'angular2/src/facade/lang';

export {RouterLinkTransform} from 'angular2/src/router/router_link_transform';

/**
* Enables the router link DSL.
*
* Warning. This feature is experimental and can change.
*
* To enable the transformer pass the router link DSL provider to `bootstrap`.
*
* ## Example:
* ```
* import {bootstrap} from 'angular2/platform/browser';
* import {ROUTER_LINK_DSL_PROVIDER} from 'angular2/router/router_link_dsl';
*
* bootstrap(CustomApp, [ROUTER_LINK_DSL_PROVIDER]);
* ```
*
* The DSL allows you to express router links as follows:
* ```
* <a [routerLink]="route:User"> <!-- Same as <a [routerLink]="['User']"> -->
* <a [routerLink]="route:/User"> <!-- Same as <a [routerLink]="['User']"> -->
* <a [routerLink]="route:./User"> <!-- Same as <a [routerLink]="['./User']"> -->
* <a [routerLink]="./User(id: value, name: 'Bob')"> <!-- Same as <a [routerLink]="['./User', {id:
* value, name: 'Bob'}]"> -->
* <a [routerLink]="/User/Modal"> <!-- Same as <a [routerLink]="['/User', 'Modal']"> -->
* <a [routerLink]="User[Modal]"> <!-- Same as <a [routerLink]="['User', ['Modal']]"> -->
* ```
*/
const ROUTER_LINK_DSL_PROVIDER =
CONST_EXPR(new Provider(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true}));
3 changes: 2 additions & 1 deletion modules/angular2/src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export {
} from './directive_metadata';
export {SourceModule, SourceWithImports} from './source_module';
export {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';

export * from 'angular2/src/compiler/template_ast';
export {TEMPLATE_TRANSFORMS} from 'angular2/src/compiler/template_parser';
import {assertionsEnabled, Type, CONST_EXPR} from 'angular2/src/facade/lang';
import {provide, Provider} from 'angular2/src/core/di';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
Expand Down
215 changes: 215 additions & 0 deletions modules/angular2/src/router/router_link_transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {
TemplateAstVisitor,
ElementAst,
BoundDirectivePropertyAst,
DirectiveAst,
BoundElementPropertyAst
} from 'angular2/compiler';
import {
AstTransformer,
Quote,
AST,
EmptyExpr,
LiteralArray,
LiteralPrimitive,
ASTWithSource
} from 'angular2/src/core/change_detection/parser/ast';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Injectable} from 'angular2/core';
import {Parser} from 'angular2/src/core/change_detection/parser/parser';

/**
* e.g., './User', 'Modal' in ./User[Modal(param: value)]
*/
class FixedPart {
constructor(public value: string) {}
}

/**
* The square bracket
*/
class AuxiliaryStart {
constructor() {}
}

/**
* The square bracket
*/
class AuxiliaryEnd {
constructor() {}
}

/**
* e.g., param:value in ./User[Modal(param: value)]
*/
class Params {
constructor(public ast: AST) {}
}

class RouterLinkLexer {
index: number = 0;

constructor(private parser: Parser, private exp: string) {}

tokenize(): Array<FixedPart | AuxiliaryStart | AuxiliaryEnd | Params> {
let tokens = [];
while (this.index < this.exp.length) {
tokens.push(this._parseToken());
}
return tokens;
}

private _parseToken() {
let c = this.exp[this.index];
if (c == '[') {
this.index++;
return new AuxiliaryStart();

} else if (c == ']') {
this.index++;
return new AuxiliaryEnd();

} else if (c == '(') {
return this._parseParams();

} else if (c == '/' && this.index !== 0) {
this.index++;
return this._parseFixedPart();

} else {
return this._parseFixedPart();
}
}

private _parseParams() {
let start = this.index;
for (; this.index < this.exp.length; ++this.index) {
let c = this.exp[this.index];
if (c == ')') {
let paramsContent = this.exp.substring(start + 1, this.index);
this.index++;
return new Params(this.parser.parseBinding(`{${paramsContent}}`, null).ast);
}
}
throw new BaseException("Cannot find ')'");
}

private _parseFixedPart() {
let start = this.index;
let sawNonSlash = false;


for (; this.index < this.exp.length; ++this.index) {
let c = this.exp[this.index];

if (c == '(' || c == '[' || c == ']' || (c == '/' && sawNonSlash)) {
break;
}

if (c != '.' && c != '/') {
sawNonSlash = true;
}
}

let fixed = this.exp.substring(start, this.index);

if (start === this.index || !sawNonSlash || fixed.startsWith('//')) {
throw new BaseException("Invalid router link");
}

return new FixedPart(fixed);
}
}

class RouterLinkAstGenerator {
index: number = 0;
constructor(private tokens: any[]) {}

generate(): AST { return this._genAuxiliary(); }

private _genAuxiliary(): AST {
let arr = [];
for (; this.index < this.tokens.length; this.index++) {
let r = this.tokens[this.index];

if (r instanceof FixedPart) {
arr.push(new LiteralPrimitive(r.value));

} else if (r instanceof Params) {
arr.push(r.ast);

} else if (r instanceof AuxiliaryEnd) {
break;

} else if (r instanceof AuxiliaryStart) {
this.index++;
arr.push(this._genAuxiliary());
}
}

return new LiteralArray(arr);
}
}

class RouterLinkAstTransformer extends AstTransformer {
constructor(private parser: Parser) { super(); }

visitQuote(ast: Quote): AST {
if (ast.prefix == "route") {
return parseRouterLinkExpression(this.parser, ast.uninterpretedExpression);
} else {
return super.visitQuote(ast);
}
}
}

export function parseRouterLinkExpression(parser: Parser, exp: string): AST {
let tokens = new RouterLinkLexer(parser, exp.trim()).tokenize();
return new RouterLinkAstGenerator(tokens).generate();
}

/**
* A compiler plugin that implements the router link DSL.
*/
@Injectable()
export class RouterLinkTransform implements TemplateAstVisitor {
private astTransformer;

constructor(parser: Parser) { this.astTransformer = new RouterLinkAstTransformer(parser); }

visitNgContent(ast: any, context: any): any { return ast; }

visitEmbeddedTemplate(ast: any, context: any): any { return ast; }

visitElement(ast: ElementAst, context: any): any {
let updatedChildren = ast.children.map(c => c.visit(this, context));
let updatedInputs = ast.inputs.map(c => c.visit(this, context));
let updatedDirectives = ast.directives.map(c => c.visit(this, context));
return new ElementAst(ast.name, ast.attrs, updatedInputs, ast.outputs, ast.exportAsVars,
updatedDirectives, updatedChildren, ast.ngContentIndex, ast.sourceSpan);
}

visitVariable(ast: any, context: any): any { return ast; }

visitEvent(ast: any, context: any): any { return ast; }

visitElementProperty(ast: any, context: any): any { return ast; }

visitAttr(ast: any, context: any): any { return ast; }

visitBoundText(ast: any, context: any): any { return ast; }

visitText(ast: any, context: any): any { return ast; }

visitDirective(ast: DirectiveAst, context: any): any {
let updatedInputs = ast.inputs.map(c => c.visit(this, context));
return new DirectiveAst(ast.directive, updatedInputs, ast.hostProperties, ast.hostEvents,
ast.exportAsVars, ast.sourceSpan);
}

visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
let transformedValue = ast.value.visit(this.astTransformer);
return new BoundDirectivePropertyAst(ast.directiveName, ast.templateName, transformedValue,
ast.sourceSpan);
}
}
25 changes: 24 additions & 1 deletion modules/angular2/test/router/integration/router_link_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
import {RootRouter} from 'angular2/src/router/router';

import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {TEMPLATE_TRANSFORMS} from 'angular2/compiler';
import {RouterLinkTransform} from 'angular2/src/router/router_link_transform';

export function main() {
describe('router-link directive', function() {
Expand All @@ -53,7 +55,8 @@ export function main() {
DirectiveResolver,
provide(Location, {useClass: SpyLocation}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: MyComp}),
provide(Router, {useClass: RootRouter})
provide(Router, {useClass: RootRouter}),
provide(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true})
]);

beforeEach(inject([TestComponentBuilder, Router, Location], (tcBuilder, rtr, loc) => {
Expand Down Expand Up @@ -320,6 +323,25 @@ export function main() {
router.navigateByUrl('/child-with-grandchild/grandchild');
});
}));


describe("router link dsl", () => {
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" [router-link]="route:./User(name: name)">{{name}}</a>')
.then((_) => router.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
.then((_) => {
fixture.debugElement.componentInstance.name = 'brian';
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('brian');
expect(DOM.getAttribute(
fixture.debugElement.componentViewChildren[0].nativeElement, 'href'))
.toEqual('/user/brian');
async.done();
});
}));
});
});

describe('when clicked', () => {
Expand Down Expand Up @@ -360,6 +382,7 @@ export function main() {
.then((_) => {
fixture.detectChanges();


var dispatchedEvent = clickOnElement(fixture);
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);

Expand Down
Loading