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)