diff --git a/gulpfile.js b/gulpfile.js
index 314d0f572e..efd7ae89fb 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -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'))
]);
});
diff --git a/modules/angular2/router/router_link_dsl.ts b/modules/angular2/router/router_link_dsl.ts
new file mode 100644
index 0000000000..84bdfb6c9f
--- /dev/null
+++ b/modules/angular2/router/router_link_dsl.ts
@@ -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:
+ * ```
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+const ROUTER_LINK_DSL_PROVIDER =
+ CONST_EXPR(new Provider(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true}));
\ No newline at end of file
diff --git a/modules/angular2/src/compiler/compiler.ts b/modules/angular2/src/compiler/compiler.ts
index a64ea755f8..fcbc6abf83 100644
--- a/modules/angular2/src/compiler/compiler.ts
+++ b/modules/angular2/src/compiler/compiler.ts
@@ -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';
diff --git a/modules/angular2/src/router/router_link_transform.ts b/modules/angular2/src/router/router_link_transform.ts
new file mode 100644
index 0000000000..0b1dc99b4e
--- /dev/null
+++ b/modules/angular2/src/router/router_link_transform.ts
@@ -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 {
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/modules/angular2/test/router/integration/router_link_spec.ts b/modules/angular2/test/router/integration/router_link_spec.ts
index 6a074c06b6..987ff4f4a5 100644
--- a/modules/angular2/test/router/integration/router_link_spec.ts
+++ b/modules/angular2/test/router/integration/router_link_spec.ts
@@ -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() {
@@ -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) => {
@@ -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('{{name}}')
+ .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', () => {
@@ -360,6 +382,7 @@ export function main() {
.then((_) => {
fixture.detectChanges();
+
var dispatchedEvent = clickOnElement(fixture);
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);
diff --git a/modules/angular2/test/router/router_link_transform_spec.ts b/modules/angular2/test/router/router_link_transform_spec.ts
new file mode 100644
index 0000000000..cc154ccf3c
--- /dev/null
+++ b/modules/angular2/test/router/router_link_transform_spec.ts
@@ -0,0 +1,68 @@
+import {
+ AsyncTestCompleter,
+ describe,
+ proxy,
+ it,
+ iit,
+ ddescribe,
+ expect,
+ inject,
+ beforeEach,
+ beforeEachBindings,
+ SpyObject
+} from 'angular2/testing_internal';
+
+import {Injector, provide} from 'angular2/core';
+import {CONST_EXPR} from 'angular2/src/facade/lang';
+
+import {parseRouterLinkExpression} from 'angular2/src/router/router_link_transform';
+import {Unparser} from '../core/change_detection/parser/unparser';
+import {Parser} from 'angular2/src/core/change_detection/parser/parser';
+
+export function main() {
+ function check(parser: Parser, input: string, expectedValue: string) {
+ let ast = parseRouterLinkExpression(parser, input);
+ expect(new Unparser().unparse(ast)).toEqual(expectedValue);
+ }
+
+ describe('parseRouterLinkExpression', () => {
+ it("should parse simple routes", inject([Parser], (p) => {
+ check(p, `User`, `["User"]`);
+ check(p, `/User`, `["/User"]`);
+ check(p, `./User`, `["./User"]`);
+ check(p, `../../User`, `["../../User"]`);
+ }));
+
+ it("should trim the string", inject([Parser], (p) => { check(p, ` User `, `["User"]`); }));
+
+ it("should parse parameters", inject([Parser], (p) => {
+ check(p, `./User(id: value, name: 'Bob')`, `["./User", {id: value, name: "Bob"}]`);
+ }));
+
+ it("should parse nested routes", inject([Parser], (p) => {
+ check(p, `User/Modal`, `["User", "Modal"]`);
+ check(p, `/User/Modal`, `["/User", "Modal"]`);
+ }));
+
+ it("should parse auxiliary routes", inject([Parser], (p) => {
+ check(p, `User[Modal]`, `["User", ["Modal"]]`);
+ check(p, `User[Modal1][Modal2]`, `["User", ["Modal1"], ["Modal2"]]`);
+ check(p, `User[Modal1[Modal2]]`, `["User", ["Modal1", ["Modal2"]]]`);
+ }));
+
+ it("should parse combinations", inject([Parser], (p) => {
+ check(p, `./User(id: value)/Post(title: 'blog')`, `["./User", {id: value}, "Post", {title: "blog"}]`);
+ check(p, `./User[Modal(param: value)]`, `["./User", ["Modal", {param: value}]]`);
+ }));
+
+ it("should error on empty fixed parts", inject([Parser], (p) => {
+ expect(() => parseRouterLinkExpression(p, `./(id: value, name: 'Bob')`))
+ .toThrowErrorWith("Invalid router link");
+ }));
+
+ it("should error on multiple slashes", inject([Parser], (p) => {
+ expect(() => parseRouterLinkExpression(p, `//User`))
+ .toThrowErrorWith("Invalid router link");
+ }));
+ });
+}
\ No newline at end of file
diff --git a/modules/playground/pubspec.yaml b/modules/playground/pubspec.yaml
index eca97838d6..97d9bdd140 100644
--- a/modules/playground/pubspec.yaml
+++ b/modules/playground/pubspec.yaml
@@ -28,6 +28,7 @@ transformers:
- web/src/model_driven_forms/index.dart
- web/src/observable_models/index.dart
- web/src/person_management/index.dart
+ - web/src/routing/index.dart
- web/src/template_driven_forms/index.dart
- web/src/zippy_component/index.dart
- web/src/material/button/index.dart
diff --git a/modules_dart/transform/lib/src/transform/common/ng_compiler.dart b/modules_dart/transform/lib/src/transform/common/ng_compiler.dart
index d6b55b8bc7..c8021d30ec 100644
--- a/modules_dart/transform/lib/src/transform/common/ng_compiler.dart
+++ b/modules_dart/transform/lib/src/transform/common/ng_compiler.dart
@@ -12,6 +12,7 @@ import 'package:angular2/src/compiler/schema/dom_element_schema_registry.dart';
import 'package:angular2/src/transform/common/asset_reader.dart';
import 'package:angular2/src/core/change_detection/interfaces.dart';
import 'package:angular2/src/compiler/change_detector_compiler.dart';
+import 'package:angular2/router/router_link_dsl.dart';
import 'xhr_impl.dart';
import 'url_resolver.dart';
@@ -23,8 +24,9 @@ TemplateCompiler createTemplateCompiler(AssetReader reader,
var _urlResolver = const TransformerUrlResolver();
// TODO(yjbanov): add router AST transformer when ready
- var templateParser = new TemplateParser(new ng.Parser(new ng.Lexer()),
- new DomElementSchemaRegistry(), _htmlParser, null);
+ var parser = new ng.Parser(new ng.Lexer());
+ var templateParser = new TemplateParser(parser,
+ new DomElementSchemaRegistry(), _htmlParser, [new RouterLinkTransform(parser)]);
var cdCompiler = changeDetectionConfig != null
? new ChangeDetectionCompiler(changeDetectionConfig)