feat(common): support as syntax in template/* bindings (#15025)

* 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
This commit is contained in:
Miško Hevery
2017-03-14 20:46:29 -07:00
committed by Chuck Jazdzewski
parent 5fe2d8fd80
commit c10c060d20
12 changed files with 137 additions and 35 deletions

View File

@ -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';

View File

@ -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];
export const COMMON_DEPRECATED_DIRECTIVES: Provider[] = [NgFor];

View File

@ -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<T> {
constructor(public $implicit: T, public index: number, public count: number) {}
/**
* @stable
*/
export class NgForOfContext<T> {
constructor(
public $implicit: T, public ngForOf: NgIterable<T>, public index: number,
public count: number) {}
get first(): boolean { return this.index === 0; }
@ -29,13 +34,21 @@ export class NgForOfRow<T> {
*
* `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<T>`: 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.
*
* ```
* <li *ngFor="let user of userObservable | async as users; indexes as i; first as isFirst">
* {{i}}/{{users.length}}. {{user}} <span *ngIf="isFirst">default</span>
* </li>
* ```
*
* ### Change Propagation
*
@ -105,11 +118,11 @@ export class NgForOf<T> implements DoCheck, OnChanges {
private _trackByFn: TrackByFunction<T>;
constructor(
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForOfRow<T>>,
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForOfContext<T>>,
private _differs: IterableDiffers) {}
@Input()
set ngForTemplate(value: TemplateRef<NgForOfRow<T>>) {
set ngForTemplate(value: TemplateRef<NgForOfContext<T>>) {
// TODO(TS2.1): make TemplateRef<Partial<NgForRowOf<T>>> 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<T> implements DoCheck, OnChanges {
(item: IterableChangeRecord<any>, 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<T> implements DoCheck, OnChanges {
} else {
const view = this._viewContainer.get(adjustedPreviousIndex);
this._viewContainer.move(view, currentIndex);
const tuple = new RecordViewTuple(item, <EmbeddedViewRef<NgForOfRow<T>>>view);
const tuple = new RecordViewTuple(item, <EmbeddedViewRef<NgForOfContext<T>>>view);
insertTuples.push(tuple);
}
});
@ -164,24 +177,26 @@ export class NgForOf<T> implements DoCheck, OnChanges {
}
for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
const viewRef = <EmbeddedViewRef<NgForOfRow<T>>>this._viewContainer.get(i);
const viewRef = <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(i);
viewRef.context.index = i;
viewRef.context.count = ilen;
}
changes.forEachIdentityChange((record: any) => {
const viewRef = <EmbeddedViewRef<NgForOfRow<T>>>this._viewContainer.get(record.currentIndex);
const viewRef =
<EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(record.currentIndex);
viewRef.context.$implicit = record.item;
});
}
private _perViewChange(view: EmbeddedViewRef<NgForOfRow<T>>, record: IterableChangeRecord<any>) {
private _perViewChange(
view: EmbeddedViewRef<NgForOfContext<T>>, record: IterableChangeRecord<any>) {
view.context.$implicit = record.item;
}
}
class RecordViewTuple<T> {
constructor(public record: any, public view: EmbeddedViewRef<NgForOfRow<T>>) {}
constructor(public record: any, public view: EmbeddedViewRef<NgForOfContext<T>>) {}
}
/**

View File

@ -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:
* ```
* <div *ngIf="condition; else elseBlock; let value">{{value}}</div>
* <div *ngIf="condition as value; else elseBlock">{{value}}</div>
* <ng-template #elseBlock>...</ng-template>
* ```
*
@ -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;
}

View File

@ -181,6 +181,14 @@ export function main() {
detectChangesAndExpectText('0|even|1|2|even|');
}));
it('should allow of saving the collection', async(() => {
const template =
'<ul><li *ngFor="let item of [1,2,3] as items; index as i">{{i}}/{{items.length}} - {{item}};</li></ul>';
fixture = createTestComponent(template);
detectChangesAndExpectText('0/3 - 1;1/3 - 2;2/3 - 3;');
}));
it('should display indices correctly', async(() => {
const template = '<span *ngFor ="let item of items; let i=index">{{i.toString()}}</span>';
fixture = createTestComponent(template);

View File

@ -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 = '<span *ngIf="booleanCondition; else elseBlock; let v">{{v}}</span>' +
'<ng-template #elseBlock let-v>{{v}}</ng-template>';
@ -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 = '<span *ngIf="booleanCondition as v; else elseBlock">{{v}}</span>' +
'<ng-template #elseBlock let-v>{{v}}</ng-template>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('true');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('false');