From c10c060d2037daffadfe7975ac584e83225400b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Tue, 14 Mar 2017 20:46:29 -0700 Subject: [PATCH] feat(common): support `as` syntax in template/* bindings (#15025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(common): support `as` syntax in template/* bindings Closes #15020 Showing the new and the equivalent old syntax. - `*ngIf="exp as var1”` => `*ngIf="exp; let var1 = ngIf”` - `*ngFor="var item of itemsStream |async as items”` => `*ngFor="var item of itemsStream |async; let items = ngForOf”` * feat(common): convert ngIf to use `*ngIf="exp as local“` syntax * feat(common): convert ngForOf to use `*ngFor=“let i of exp as local“` syntax * feat(common): expose NgForOfContext and NgIfContext --- packages/common/src/common.ts | 2 +- packages/common/src/directives/index.ts | 8 +-- packages/common/src/directives/ng_for_of.ts | 49 ++++++++++++------- packages/common/src/directives/ng_if.ts | 14 ++++-- .../common/test/directives/ng_for_spec.ts | 8 +++ packages/common/test/directives/ng_if_spec.ts | 16 +++++- .../compiler/src/expression_parser/lexer.ts | 4 +- .../compiler/src/expression_parser/parser.ts | 18 ++++++- .../test/expression_parser/parser_spec.ts | 10 ++++ .../template_parser/template_parser_spec.ts | 12 +++++ packages/examples/common/ngIf/ts/module.ts | 8 +-- .../common/typings/common.d.ts | 23 ++++++++- 12 files changed, 137 insertions(+), 35 deletions(-) diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index ea845f392f..21889ba297 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -14,7 +14,7 @@ export * from './location/index'; export {NgLocaleLocalization, NgLocalization} from './localization'; export {CommonModule} from './common_module'; -export {NgClass, NgFor, NgForOf, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; +export {NgClass, NgFor, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index'; export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id'; export {VERSION} from './version'; diff --git a/packages/common/src/directives/index.ts b/packages/common/src/directives/index.ts index 74497ba25a..566def21f9 100644 --- a/packages/common/src/directives/index.ts +++ b/packages/common/src/directives/index.ts @@ -10,8 +10,8 @@ import {Provider} from '@angular/core'; import {NgClass} from './ng_class'; import {NgComponentOutlet} from './ng_component_outlet'; -import {NgFor, NgForOf} from './ng_for_of'; -import {NgIf} from './ng_if'; +import {NgFor, NgForOf, NgForOfContext} from './ng_for_of'; +import {NgIf, NgIfContext} from './ng_if'; import {NgPlural, NgPluralCase} from './ng_plural'; import {NgStyle} from './ng_style'; import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch'; @@ -22,7 +22,9 @@ export { NgComponentOutlet, NgFor, NgForOf, + NgForOfContext, NgIf, + NgIfContext, NgPlural, NgPluralCase, NgStyle, @@ -55,4 +57,4 @@ export const COMMON_DIRECTIVES: Provider[] = [ /** * A colletion of deprecated directives that are no longer part of the core module. */ -export const COMMON_DEPRECATED_DIRECTIVES: Provider[] = [NgFor]; \ No newline at end of file +export const COMMON_DEPRECATED_DIRECTIVES: Provider[] = [NgFor]; diff --git a/packages/common/src/directives/ng_for_of.ts b/packages/common/src/directives/ng_for_of.ts index 93f04f7c2f..1f2a8dad55 100644 --- a/packages/common/src/directives/ng_for_of.ts +++ b/packages/common/src/directives/ng_for_of.ts @@ -8,8 +8,13 @@ import {ChangeDetectorRef, Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, OnChanges, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode} from '@angular/core'; -export class NgForOfRow { - constructor(public $implicit: T, public index: number, public count: number) {} +/** + * @stable + */ +export class NgForOfContext { + constructor( + public $implicit: T, public ngForOf: NgIterable, public index: number, + public count: number) {} get first(): boolean { return this.index === 0; } @@ -29,13 +34,21 @@ export class NgForOfRow { * * `NgForOf` provides several exported values that can be aliased to local variables: * - * * `index` will be set to the current loop iteration for each template context. - * * `first` will be set to a boolean value indicating whether the item is the first one in the - * iteration. - * * `last` will be set to a boolean value indicating whether the item is the last one in the - * iteration. - * * `even` will be set to a boolean value indicating whether this item has an even index. - * * `odd` will be set to a boolean value indicating whether this item has an odd index. + * - `$implicit: T`: The value of the individual items in the iterable (`ngForOf`). + * - `ngForOf: NgIterable`: The value of the iterable expression. Useful when the expression is + * more complex then a property access, for example when using the async pipe (`userStreams | + * async`). + * - `index: number`: The index of the current item in the iterable. + * - `first: boolean`: True when the item is the first item in the iterable. + * - `last: boolean`: True when the item is the last item in the iterable. + * - `even: boolean`: True when the item has an even index in the iterable. + * - `odd: boolean`: True when the item has an odd index in the iterable. + * + * ``` + *
  • + * {{i}}/{{users.length}}. {{user}} default + *
  • + * ``` * * ### Change Propagation * @@ -105,11 +118,11 @@ export class NgForOf implements DoCheck, OnChanges { private _trackByFn: TrackByFunction; constructor( - private _viewContainer: ViewContainerRef, private _template: TemplateRef>, + private _viewContainer: ViewContainerRef, private _template: TemplateRef>, private _differs: IterableDiffers) {} @Input() - set ngForTemplate(value: TemplateRef>) { + set ngForTemplate(value: TemplateRef>) { // TODO(TS2.1): make TemplateRef>> once we move to TS v2.1 // The current type is too restrictive; a template that just uses index, for example, // should be acceptable. @@ -146,7 +159,7 @@ export class NgForOf implements DoCheck, OnChanges { (item: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { if (item.previousIndex == null) { const view = this._viewContainer.createEmbeddedView( - this._template, new NgForOfRow(null, null, null), currentIndex); + this._template, new NgForOfContext(null, this.ngForOf, null, null), currentIndex); const tuple = new RecordViewTuple(item, view); insertTuples.push(tuple); } else if (currentIndex == null) { @@ -154,7 +167,7 @@ export class NgForOf implements DoCheck, OnChanges { } else { const view = this._viewContainer.get(adjustedPreviousIndex); this._viewContainer.move(view, currentIndex); - const tuple = new RecordViewTuple(item, >>view); + const tuple = new RecordViewTuple(item, >>view); insertTuples.push(tuple); } }); @@ -164,24 +177,26 @@ export class NgForOf implements DoCheck, OnChanges { } for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) { - const viewRef = >>this._viewContainer.get(i); + const viewRef = >>this._viewContainer.get(i); viewRef.context.index = i; viewRef.context.count = ilen; } changes.forEachIdentityChange((record: any) => { - const viewRef = >>this._viewContainer.get(record.currentIndex); + const viewRef = + >>this._viewContainer.get(record.currentIndex); viewRef.context.$implicit = record.item; }); } - private _perViewChange(view: EmbeddedViewRef>, record: IterableChangeRecord) { + private _perViewChange( + view: EmbeddedViewRef>, record: IterableChangeRecord) { view.context.$implicit = record.item; } } class RecordViewTuple { - constructor(public record: any, public view: EmbeddedViewRef>) {} + constructor(public record: any, public view: EmbeddedViewRef>) {} } /** diff --git a/packages/common/src/directives/ng_if.ts b/packages/common/src/directives/ng_if.ts index ccb28a47f0..2cb437d562 100644 --- a/packages/common/src/directives/ng_if.ts +++ b/packages/common/src/directives/ng_if.ts @@ -61,7 +61,7 @@ import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from ' * A better way to do this is to use `ngIf` and store the result of the condition in a local * variable as shown in the the example below: * - * {@example common/ngIf/ts/module.ts region='NgIfLet'} + * {@example common/ngIf/ts/module.ts region='NgIfAs'} * * Notice that: * - We use only one `async` pipe and hence only one subscription gets created. @@ -93,7 +93,7 @@ import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from ' * * Form with storing the value locally: * ``` - *
    {{value}}
    + *
    {{value}}
    * ... * ``` * @@ -113,7 +113,7 @@ export class NgIf { @Input() set ngIf(condition: any) { - this._context.$implicit = condition; + this._context.$implicit = this._context.ngIf = condition; this._updateView(); } @@ -154,4 +154,10 @@ export class NgIf { } } -export class NgIfContext { public $implicit: any = null; } +/** + * @stable + */ +export class NgIfContext { + public $implicit: any = null; + public ngIf: any = null; +} diff --git a/packages/common/test/directives/ng_for_spec.ts b/packages/common/test/directives/ng_for_spec.ts index 4c5829c679..5a58a10c4f 100644 --- a/packages/common/test/directives/ng_for_spec.ts +++ b/packages/common/test/directives/ng_for_spec.ts @@ -181,6 +181,14 @@ export function main() { detectChangesAndExpectText('0|even|1|2|even|'); })); + it('should allow of saving the collection', async(() => { + const template = + '
    • {{i}}/{{items.length}} - {{item}};
    '; + fixture = createTestComponent(template); + + detectChangesAndExpectText('0/3 - 1;1/3 - 2;2/3 - 3;'); + })); + it('should display indices correctly', async(() => { const template = '{{i.toString()}}'; fixture = createTestComponent(template); diff --git a/packages/common/test/directives/ng_if_spec.ts b/packages/common/test/directives/ng_if_spec.ts index c1c17f6235..43727cc347 100644 --- a/packages/common/test/directives/ng_if_spec.ts +++ b/packages/common/test/directives/ng_if_spec.ts @@ -189,7 +189,7 @@ export function main() { expect(fixture.nativeElement).toHaveText('FALSE2'); })); - it('should support binding to variable', async(() => { + it('should support binding to variable using let', async(() => { const template = '{{v}}' + '{{v}}'; @@ -198,6 +198,20 @@ export function main() { fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('true'); + getComponent().booleanCondition = false; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('false'); + })); + + it('should support binding to variable using as', async(() => { + const template = '{{v}}' + + '{{v}}'; + + fixture = createTestComponent(template); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('true'); + getComponent().booleanCondition = false; fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('false'); diff --git a/packages/compiler/src/expression_parser/lexer.ts b/packages/compiler/src/expression_parser/lexer.ts index d097d9b5ce..7fe5a3a970 100644 --- a/packages/compiler/src/expression_parser/lexer.ts +++ b/packages/compiler/src/expression_parser/lexer.ts @@ -19,7 +19,7 @@ export enum TokenType { Error } -const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this']; +const KEYWORDS = ['var', 'let', 'as', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this']; @CompilerInjectable() export class Lexer { @@ -58,6 +58,8 @@ export class Token { isKeywordLet(): boolean { return this.type == TokenType.Keyword && this.strValue == 'let'; } + isKeywordAs(): boolean { return this.type == TokenType.Keyword && this.strValue == 'as'; } + isKeywordNull(): boolean { return this.type == TokenType.Keyword && this.strValue == 'null'; } isKeywordUndefined(): boolean { diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index d2e5ec0f60..3caad72696 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -267,6 +267,7 @@ export class _ParseAST { } peekKeywordLet(): boolean { return this.next.isKeywordLet(); } + peekKeywordAs(): boolean { return this.next.isKeywordAs(); } expectCharacter(code: number) { if (this.optionalCharacter(code)) return; @@ -686,11 +687,12 @@ export class _ParseAST { const warnings: string[] = []; while (this.index < this.tokens.length) { const start = this.inputIndex; - const keyIsVar: boolean = this.peekKeywordLet(); + let keyIsVar: boolean = this.peekKeywordLet(); if (keyIsVar) { this.advance(); } - let key = this.expectTemplateBindingKey(); + let rawKey = this.expectTemplateBindingKey(); + let key = rawKey; if (!keyIsVar) { if (prefix == null) { prefix = key; @@ -707,6 +709,12 @@ export class _ParseAST { } else { name = '\$implicit'; } + } else if (this.peekKeywordAs()) { + const letStart = this.inputIndex; + this.advance(); // consume `as` + name = rawKey; + key = this.expectTemplateBindingKey(); // read local var name + keyIsVar = true; } else if (this.next !== EOF && !this.peekKeywordLet()) { const start = this.inputIndex; const ast = this.parsePipe(); @@ -714,6 +722,12 @@ export class _ParseAST { expression = new ASTWithSource(ast, source, this.location, this.errors); } bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression)); + if (this.peekKeywordAs() && !keyIsVar) { + const letStart = this.inputIndex; + this.advance(); // consume `as` + const letName = this.expectTemplateBindingKey(); // read local var name + bindings.push(new TemplateBinding(this.span(letStart), letName, true, key, null)); + } if (!this.optionalCharacter(chars.$SEMICOLON)) { this.optionalCharacter(chars.$COMMA); } diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 50f2c76c5e..d321fd2df5 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -429,6 +429,16 @@ export function main() { ]); }); + it('should support as notation', () => { + let bindings = parseTemplateBindings('ngIf exp as local', 'location'); + expect(keyValues(bindings)).toEqual(['ngIf=exp in location', 'let local=ngIf']); + + bindings = parseTemplateBindings('ngFor let item of items as iter; index as i', 'L'); + expect(keyValues(bindings)).toEqual([ + 'ngFor', 'let item=$implicit', 'ngForOf=items in L', 'let iter=ngForOf', 'let i=index' + ]); + }); + it('should parse pipes', () => { const bindings = parseTemplateBindings('key value|pipe'); const ast = bindings[0].expression.ast; diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index c87ef9cf3e..862fe64866 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -1334,6 +1334,18 @@ Reference "#a" is defined several times ("
    ]#a>
    expect(humanizeTplAst(parse('
    ', []))).toEqual(targetAst); }); + it('should parse variables via as ...', () => { + const targetAst = [ + [EmbeddedTemplateAst], + [VariableAst, 'local', 'ngIf'], + [DirectiveAst, ngIf], + [BoundDirectivePropertyAst, 'ngIf', 'expr'], + [ElementAst, 'div'], + ]; + + expect(humanizeTplAst(parse('
    ', [ngIf]))).toEqual(targetAst); + }); + describe('directives', () => { it('should locate directives in property bindings', () => { const dirA = diff --git a/packages/examples/common/ngIf/ts/module.ts b/packages/examples/common/ngIf/ts/module.ts index 82e805a568..1473bd9254 100644 --- a/packages/examples/common/ngIf/ts/module.ts +++ b/packages/examples/common/ngIf/ts/module.ts @@ -73,19 +73,19 @@ class NgIfThenElse implements OnInit { } // #enddocregion -// #docregion NgIfLet +// #docregion NgIfAs @Component({ selector: 'ng-if-let', template: `
    -
    +
    Hello {{user.last}}, {{user.first}}!
    Waiting... (user is {{user|json}}) ` }) -class NgIfLet { +class NgIfAs { userObservable = new Subject<{first: string, last: string}>(); first = ['John', 'Mike', 'Mary', 'Bob']; firstIndex = 0; @@ -121,7 +121,7 @@ class ExampleApp { @NgModule({ imports: [BrowserModule], - declarations: [ExampleApp, NgIfSimple, NgIfElse, NgIfThenElse, NgIfLet], + declarations: [ExampleApp, NgIfSimple, NgIfElse, NgIfThenElse, NgIfAs], bootstrap: [ExampleApp] }) export class AppModule { diff --git a/tools/public_api_guard/common/typings/common.d.ts b/tools/public_api_guard/common/typings/common.d.ts index 8cfc4a2126..12b08c4202 100644 --- a/tools/public_api_guard/common/typings/common.d.ts +++ b/tools/public_api_guard/common/typings/common.d.ts @@ -151,13 +151,26 @@ export declare const NgFor: typeof NgForOf; /** @stable */ export declare class NgForOf implements DoCheck, OnChanges { ngForOf: NgIterable; - ngForTemplate: TemplateRef>; + ngForTemplate: TemplateRef>; ngForTrackBy: TrackByFunction; - constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers); + constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers); ngDoCheck(): void; ngOnChanges(changes: SimpleChanges): void; } +/** @stable */ +export declare class NgForOfContext { + $implicit: T; + count: number; + readonly even: boolean; + readonly first: boolean; + index: number; + readonly last: boolean; + ngForOf: NgIterable; + readonly odd: boolean; + constructor($implicit: T, ngForOf: NgIterable, index: number, count: number); +} + /** @stable */ export declare class NgIf { ngIf: any; @@ -166,6 +179,12 @@ export declare class NgIf { constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef); } +/** @stable */ +export declare class NgIfContext { + $implicit: any; + ngIf: any; +} + /** @experimental */ export declare class NgLocaleLocalization extends NgLocalization { protected locale: string;