parent
e67e1952d0
commit
4ea5b6e57f
20
gulpfile.js
20
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'))
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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}));
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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('<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', () => {
|
||||
|
@ -360,6 +382,7 @@ export function main() {
|
|||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
var dispatchedEvent = clickOnElement(fixture);
|
||||
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);
|
||||
|
||||
|
|
|
@ -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");
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue