feat: ngIf now supports else; saves condition to local var

NgIf syntax has been extended to support else clause to display template
when the condition is false. In addition the condition value can now
be stored in local variable, for later reuse. This is especially useful
when used with the `async` pipe.

Example:

```
<div *ngIf="userObservable | async; else loading; let user">
  Hello {{user.last}}, {{user.first}}!
</div>
<template #loading>Waiting...</template>
```

closes #13061
closes #13297
This commit is contained in:
Misko Hevery 2016-12-07 21:41:27 -08:00 committed by Victor Berchet
parent e15a3f273f
commit b4db73d0bf
7 changed files with 402 additions and 22 deletions

View File

@ -6,46 +6,152 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';
import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from '@angular/core';
/**
* Removes or recreates a portion of the DOM tree based on an {expression}.
* Conditionally includes a template based on the value of an `expression`.
*
* If the expression assigned to `ngIf` evaluates to a falsy value then the element
* is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
* `ngIf` evaluates the `expression` and then renders the `then` or `else` template in its place
* when expression is thruthy or falsy respectively. Typically the:
* - `then` template is the inline template of `ngIf` unless bound to a different value.
* - `else` template is blank unless its bound.
*
* ### Example ([live demo](http://plnkr.co/edit/fe0kgemFBtmQOY31b4tw?p=preview)):
* # Most common usage
*
* The most common usage of the `ngIf` is to conditionally show the inline template as seen in this
* example:
* {@example common/ngIf/ts/module.ts region='NgIfSimple'}
*
* # Showing an alternative template using `else`
*
* If it is necessary to display a template when the `expression` is falsy use the `else` template
* binding as shown. Note that the `else` binding points to a `<template>` labeled `#elseBlock`.
* The template can be defined anywhere in the component view but is typically placed right after
* `ngIf` for readability.
*
* {@example common/ngIf/ts/module.ts region='NgIfElse'}
*
* # Using non-inlined `then` template
*
* Usually the `then` template is the inlined template of the `ngIf`, but it can be changed using
* a binding (just like `else`). Because `then` and `else` are bindings, the template references can
* change at runtime as shown in thise example.
*
* {@example common/ngIf/ts/module.ts region='NgIfThenElse'}
*
* # Storing conditional result in a variable
*
* A common patter is that we need to show a set of properties from the same object. if the
* object is undefined, then we have to use the safe-traversal-operator `?.` to guard against
* dereferencing a `null` value. This is especially the case when waiting on async data such as
* when using the `async` pipe as shown in folowing example:
*
* ```
* <div *ngIf="errorCount > 0" class="error">
* <!-- Error message displayed when the errorCount property in the current context is greater
* than 0. -->
* {{errorCount}} errors detected
* </div>
* Hello {{ (userStream|async)?.last }}, {{ (userStream|async)?.first }}!
* ```
*
* There are several inefficiencies in the above example.
* - We create multiple subscriptions on the `userStream`. One for each `async` pipe, or two
* as shown in the example above.
* - We can not display an alternative screen while waiting for the data to arrive asynchronously.
* - We have to use the safe-traversal-operator `?.` to access properties, which is cumbersome.
* - We have to place the `async` pipe in parenthesis.
*
* 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'}
*
* Notice that:
* - We use only one `async` pipe and hence only one subscription gets created.
* - `ngIf` stores the result of the `userStream|async` in the local variable `user`.
* - The local `user` can than be bound repeatedly in a more efficient way.
* - No need to use the safe-traversal-operator `?.` to access properties as `ngIf` will only
* display the data if `userStream` returns a value.
* - We can display an alternative template while waiting for the data.
*
* ### Syntax
*
* Simple form:
* - `<div *ngIf="condition">...</div>`
* - `<div template="ngIf condition">...</div>`
* - `<template [ngIf]="condition"><div>...</div></template>`
* - `<ng-container [ngIf]="condition"><div>...</div></ng-container>`
*
* Form with an else block:
* ```
* <div *ngIf="condition; else elseBlock">...</div>
* <template #elseBlock>...</template>
* ```
*
* Form with a `then` and `else` block:
* ```
* <div *ngIf="condition; then thenBlock else elseBlock"></div>
* <template #thenBlock>...</template>
* <template #elseBlock>...</template>
* ```
*
* Form with storing the value locally:
* ```
* <div *ngIf="condition; else elseBlock; let value">{{value}}</div>
* <template #elseBlock>...</template>
* ```
*
* @stable
*/
@Directive({selector: '[ngIf]'})
export class NgIf {
private _hasView = false;
private _context: NgIfContext = new NgIfContext();
private _thenTemplateRef: TemplateRef<NgIfContext> = null;
private _elseTemplateRef: TemplateRef<NgIfContext> = null;
private _thenViewRef: EmbeddedViewRef<NgIfContext> = null;
private _elseViewRef: EmbeddedViewRef<NgIfContext> = null;
constructor(private _viewContainer: ViewContainerRef, private _template: TemplateRef<Object>) {}
constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>) {
this._thenTemplateRef = templateRef;
}
@Input()
set ngIf(condition: any) {
if (condition && !this._hasView) {
this._hasView = true;
this._viewContainer.createEmbeddedView(this._template);
} else if (!condition && this._hasView) {
this._hasView = false;
this._viewContainer.clear();
this._context.$implicit = condition;
this._updateView();
}
@Input()
set ngIfThen(templateRef: TemplateRef<NgIfContext>) {
this._thenTemplateRef = templateRef;
this._thenViewRef = null; // clear previous view if any.
this._updateView();
}
@Input()
set ngIfElse(templateRef: TemplateRef<NgIfContext>) {
this._elseTemplateRef = templateRef;
this._elseViewRef = null; // clear previous view if any.
this._updateView();
}
private _updateView() {
if (this._context.$implicit) {
if (!this._thenViewRef) {
this._viewContainer.clear();
this._elseViewRef = null;
if (this._thenTemplateRef) {
this._thenViewRef =
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
}
}
} else {
if (!this._elseViewRef) {
this._viewContainer.clear();
this._thenViewRef = null;
if (this._elseTemplateRef) {
this._elseViewRef =
this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
}
}
}
}
}
export class NgIfContext { public $implicit: any = null; }

View File

@ -153,6 +153,78 @@ export function main() {
expect(getDOM().hasClass(getDOM().querySelector(fixture.nativeElement, 'span'), 'foo'))
.toBe(true);
}));
describe('else', () => {
it('should support else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else elseBlock">TRUE</span>' +
'<template #elseBlock>FALSE</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('TRUE');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE');
}));
it('should support then and else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; then thenBlock; else elseBlock">IGNORE</span>' +
'<template #thenBlock>THEN</template>' +
'<template #elseBlock>ELSE</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('THEN');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('ELSE');
}));
it('should support dynamic else', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else nestedBooleanCondition ? b1 : b2">TRUE</span>' +
'<template #b1>FALSE1</template>' +
'<template #b2>FALSE2</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('TRUE');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE1');
getComponent().nestedBooleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('FALSE2');
}));
it('should support binding to variable', async(() => {
const template = '<div>' +
'<span *ngIf="booleanCondition; else elseBlock; let v">{{v}}</span>' +
'<template #elseBlock let-v>{{v}}</template>' +
'</div>';
fixture = createTestComponent(template);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('true');
getComponent().booleanCondition = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('false');
}));
});
});
}

View File

@ -0,0 +1,72 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {$, ExpectedConditions, browser, by, element} from 'protractor';
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
function waitForElement(selector: string) {
const EC = ExpectedConditions;
// Waits for the element with id 'abc' to be present on the dom.
browser.wait(EC.presenceOf($(selector)), 20000);
}
describe('ngIf', () => {
const URL = 'common/ngIf/ts/';
afterEach(verifyNoBrowserErrors);
describe('ng-if-simple', () => {
var comp = 'ng-if-simple'
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText()).toEqual('hide show = true\nText to show');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText()).toEqual('show show = false');
});
});
describe('ng-if-else', () => {
var comp = 'ng-if-else'
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText()).toEqual('hide show = true\nText to show');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('show show = false\nAlternate text while primary text is hidden');
});
});
describe('ng-if-then-else', () => {
var comp = 'ng-if-then-else'
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('hide Switch Primary show = true\nPrimary text to show');
element.all(by.css(comp + ' button')).get(1).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('hide Switch Primary show = true\nSecondary text to show');
element.all(by.css(comp + ' button')).get(0).click();
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('show Switch Primary show = false\nAlternate text while primary text is hidden');
});
});
describe('ng-if-let', () => {
var comp = 'ng-if-let'
it('should hide/show content', () => {
browser.get(URL);
waitForElement(comp);
expect(element.all(by.css(comp)).get(0).getText())
.toEqual('Next User\nWaiting... (user is null)');
element(by.css(comp + ' button')).click();
expect(element.all(by.css(comp)).get(0).getText()).toEqual('Next User\nHello Smith, John!');
});
});
});

View File

@ -0,0 +1,128 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule, OnInit, TemplateRef, ViewChild} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {Subject} from 'rxjs/Subject';
// #docregion NgIfSimple
@Component({
selector: 'ng-if-simple',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
show = {{show}}
<br>
<div *ngIf="show">Text to show</div>
`
})
class NgIfSimple {
show: boolean = true;
}
// #enddocregion
// #docregion NgIfElse
@Component({
selector: 'ng-if-else',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
show = {{show}}
<br>
<div *ngIf="show; else elseBlock">Text to show</div>
<template #elseBlock>Alternate text while primary text is hidden</template>
`
})
class NgIfElse {
show: boolean = true;
}
// #enddocregion
// #docregion NgIfThenElse
@Component({
selector: 'ng-if-then-else',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
<button (click)="switchPrimary()">Switch Primary</button>
show = {{show}}
<br>
<div *ngIf="show; then thenBlock; else elseBlock">this is ignored</div>
<template #primaryBlock>Primary text to show</template>
<template #secondaryBlock>Secondary text to show</template>
<template #elseBlock>Alternate text while primary text is hidden</template>
`
})
class NgIfThenElse implements OnInit {
thenBlock: TemplateRef<any> = null;
show: boolean = true;
@ViewChild('primaryBlock')
primaryBlock: TemplateRef<any> = null;
@ViewChild('secondaryBlock')
secondaryBlock: TemplateRef<any> = null;
switchPrimary() {
this.thenBlock = this.thenBlock === this.primaryBlock ? this.secondaryBlock : this.primaryBlock;
}
ngOnInit() { this.thenBlock = this.primaryBlock; }
}
// #enddocregion
// #docregion NgIfLet
@Component({
selector: 'ng-if-let',
template: `
<button (click)="nextUser()">Next User</button>
<br>
<div *ngIf="userObservable | async; else loading; let user">
Hello {{user.last}}, {{user.first}}!
</div>
<template #loading let-user>Waiting... (user is {{user|json}})</template>
`
})
class NgIfLet {
userObservable = new Subject<{first: string, last: string}>();
first = ['John', 'Mike', 'Mary', 'Bob'];
firstIndex = 0;
last = ['Smith', 'Novotny', 'Angular'];
lastIndex = 0;
nextUser() {
var first = this.first[this.firstIndex++];
if (this.firstIndex >= this.first.length) this.firstIndex = 0;
var last = this.last[this.lastIndex++];
if (this.lastIndex >= this.last.length) this.lastIndex = 0;
this.userObservable.next({first, last});
}
}
// #enddocregion
@Component({
selector: 'example-app',
template: `
<ng-if-simple></ng-if-simple>
<hr>
<ng-if-else></ng-if-else>
<hr>
<ng-if-then-else></ng-if-then-else>
<hr>
<ng-if-let></ng-if-let>
<hr>
`
})
class ExampleApp {
}
@NgModule({
imports: [BrowserModule],
declarations: [ExampleApp, NgIfSimple, NgIfElse, NgIfThenElse, NgIfLet],
bootstrap: [ExampleApp]
})
export class AppModule {
}

View File

@ -213,7 +213,7 @@ export class UnknownTrackBy {
'ng-if-cases.ts': `
import {Component} from '@angular/core';
@Component({template: '<div ~{implicit}*ngIf="show; let l"~{implicit-end}>Showing now!</div>'})
@Component({template: '<div ~{implicit}*ngIf="show; let l=unknown"~{implicit-end}>Showing now!</div>'})
export class ShowIf {
show = false;
}

View File

@ -186,7 +186,7 @@ describe('plugin', () => {
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
}
it('should report an implicit context reference', () => {
expectError('implicit', 'The template context does not have an implicit value');
expectError('implicit', "The template context does not defined a member called 'unknown'");
});
});
});

View File

@ -130,7 +130,9 @@ export declare class NgFor implements DoCheck, OnChanges {
/** @stable */
export declare class NgIf {
ngIf: any;
constructor(_viewContainer: ViewContainerRef, _template: TemplateRef<Object>);
ngIfElse: TemplateRef<NgIfContext>;
ngIfThen: TemplateRef<NgIfContext>;
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>);
}
/** @experimental */