fix(common): NgSwitch - don’t create the default case if another case matches (#12726)

This also simplifies the implementation of `NgSwitch`.

Closes #11297
Closes #9420
This commit is contained in:
Tobias Bosch 2016-11-07 12:22:36 -08:00 committed by vikerman
parent 32fcec9fcb
commit d8f23f4b7f
3 changed files with 196 additions and 167 deletions

View File

@ -6,19 +6,31 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; import {Directive, DoCheck, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core';
import {ListWrapper} from '../facade/collection';
const _CASE_DEFAULT = {};
export class SwitchView { export class SwitchView {
private _created = false;
constructor( constructor(
private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef<Object>) {} private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef<Object>) {}
create(): void { this._viewContainerRef.createEmbeddedView(this._templateRef); } create(): void {
this._created = true;
this._viewContainerRef.createEmbeddedView(this._templateRef);
}
destroy(): void { this._viewContainerRef.clear(); } destroy(): void {
this._created = false;
this._viewContainerRef.clear();
}
enforceState(created: boolean) {
if (created && !this._created) {
this.create();
} else if (!created && this._created) {
this.destroy();
}
}
} }
/** /**
@ -64,92 +76,52 @@ export class SwitchView {
*/ */
@Directive({selector: '[ngSwitch]'}) @Directive({selector: '[ngSwitch]'})
export class NgSwitch { export class NgSwitch {
private _switchValue: any; private _defaultViews: SwitchView[];
private _useDefault: boolean = false; private _defaultUsed = false;
private _valueViews = new Map<any, SwitchView[]>(); private _caseCount = 0;
private _activeViews: SwitchView[] = []; private _lastCaseCheckIndex = 0;
private _lastCasesMatched = false;
private _ngSwitch: any;
@Input() @Input()
set ngSwitch(value: any) { set ngSwitch(newValue: any) {
// Set of views to display for this value this._ngSwitch = newValue;
let views = this._valueViews.get(value); if (this._caseCount === 0) {
this._updateDefaultCases(true);
if (views) {
this._useDefault = false;
} else {
// No view to display for the current value -> default case
// Nothing to do if the default case was already active
if (this._useDefault) {
return;
}
this._useDefault = true;
views = this._valueViews.get(_CASE_DEFAULT);
}
this._emptyAllActiveViews();
this._activateViews(views);
this._switchValue = value;
}
/** @internal */
_onCaseValueChanged(oldCase: any, newCase: any, view: SwitchView): void {
this._deregisterView(oldCase, view);
this._registerView(newCase, view);
if (oldCase === this._switchValue) {
view.destroy();
ListWrapper.remove(this._activeViews, view);
} else if (newCase === this._switchValue) {
if (this._useDefault) {
this._useDefault = false;
this._emptyAllActiveViews();
}
view.create();
this._activeViews.push(view);
}
// Switch to default when there is no more active ViewContainers
if (this._activeViews.length === 0 && !this._useDefault) {
this._useDefault = true;
this._activateViews(this._valueViews.get(_CASE_DEFAULT));
}
}
private _emptyAllActiveViews(): void {
const activeContainers = this._activeViews;
for (var i = 0; i < activeContainers.length; i++) {
activeContainers[i].destroy();
}
this._activeViews = [];
}
private _activateViews(views?: SwitchView[]): void {
if (views) {
for (var i = 0; i < views.length; i++) {
views[i].create();
}
this._activeViews = views;
} }
} }
/** @internal */ /** @internal */
_registerView(value: any, view: SwitchView): void { _addCase(): number { return this._caseCount++; }
let views = this._valueViews.get(value);
if (!views) { /** @internal */
views = []; _addDefault(view: SwitchView) {
this._valueViews.set(value, views); if (!this._defaultViews) {
this._defaultViews = [];
} }
views.push(view); this._defaultViews.push(view);
} }
private _deregisterView(value: any, view: SwitchView): void { /** @internal */
// `_CASE_DEFAULT` is used a marker for non-registered cases _matchCase(value: any): boolean {
if (value === _CASE_DEFAULT) return; const matched = value == this._ngSwitch;
const views = this._valueViews.get(value); this._lastCasesMatched = this._lastCasesMatched || matched;
if (views.length == 1) { this._lastCaseCheckIndex++;
this._valueViews.delete(value); if (this._lastCaseCheckIndex === this._caseCount) {
} else { this._updateDefaultCases(!this._lastCasesMatched);
ListWrapper.remove(views, view); this._lastCaseCheckIndex = 0;
this._lastCasesMatched = false;
}
return matched;
}
private _updateDefaultCases(useDefault: boolean) {
if (this._defaultViews && useDefault !== this._defaultUsed) {
this._defaultUsed = useDefault;
for (var i = 0; i < this._defaultViews.length; i++) {
const defaultView = this._defaultViews[i];
defaultView.enforceState(useDefault);
}
} }
} }
} }
@ -179,24 +151,20 @@ export class NgSwitch {
* @stable * @stable
*/ */
@Directive({selector: '[ngSwitchCase]'}) @Directive({selector: '[ngSwitchCase]'})
export class NgSwitchCase { export class NgSwitchCase implements DoCheck {
// `_CASE_DEFAULT` is used as a marker for a not yet initialized value
private _value: any = _CASE_DEFAULT;
private _view: SwitchView; private _view: SwitchView;
private _switch: NgSwitch;
@Input()
ngSwitchCase: any;
constructor( constructor(
viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
@Host() ngSwitch: NgSwitch) { @Host() private ngSwitch: NgSwitch) {
this._switch = ngSwitch; ngSwitch._addCase();
this._view = new SwitchView(viewContainer, templateRef); this._view = new SwitchView(viewContainer, templateRef);
} }
@Input() ngDoCheck() { this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase)); }
set ngSwitchCase(value: any) {
this._switch._onCaseValueChanged(this._value, value, this._view);
this._value = value;
}
} }
/** /**
@ -226,7 +194,7 @@ export class NgSwitchCase {
export class NgSwitchDefault { export class NgSwitchDefault {
constructor( constructor(
viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>,
@Host() sswitch: NgSwitch) { @Host() ngSwitch: NgSwitch) {
sswitch._registerView(_CASE_DEFAULT, new SwitchView(viewContainer, templateRef)); ngSwitch._addDefault(new SwitchView(viewContainer, templateRef));
} }
} }

View File

@ -7,8 +7,8 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component} from '@angular/core'; import {Attribute, Component, Directive} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
export function main() { export function main() {
@ -32,93 +32,153 @@ export function main() {
}); });
describe('switch value changes', () => { describe('switch value changes', () => {
it('should switch amongst when values', async(() => { it('should switch amongst when values', () => {
const template = '<div>' + const template = '<div>' +
'<ul [ngSwitch]="switchValue">' + '<ul [ngSwitch]="switchValue">' +
'<template ngSwitchCase="a"><li>when a</li></template>' + '<template ngSwitchCase="a"><li>when a</li></template>' +
'<template ngSwitchCase="b"><li>when b</li></template>' + '<template ngSwitchCase="b"><li>when b</li></template>' +
'</ul></div>'; '</ul></div>';
fixture = createTestComponent(template); fixture = createTestComponent(template);
detectChangesAndExpectText(''); detectChangesAndExpectText('');
getComponent().switchValue = 'a'; getComponent().switchValue = 'a';
detectChangesAndExpectText('when a'); detectChangesAndExpectText('when a');
getComponent().switchValue = 'b'; getComponent().switchValue = 'b';
detectChangesAndExpectText('when b'); detectChangesAndExpectText('when b');
})); });
it('should switch amongst when values with fallback to default', async(() => { it('should switch amongst when values with fallback to default', () => {
const template = '<div>' + const template = '<div>' +
'<ul [ngSwitch]="switchValue">' + '<ul [ngSwitch]="switchValue">' +
'<li template="ngSwitchCase \'a\'">when a</li>' + '<li template="ngSwitchCase \'a\'">when a</li>' +
'<li template="ngSwitchDefault">when default</li>' + '<li template="ngSwitchDefault">when default</li>' +
'</ul></div>'; '</ul></div>';
fixture = createTestComponent(template); fixture = createTestComponent(template);
detectChangesAndExpectText('when default'); detectChangesAndExpectText('when default');
getComponent().switchValue = 'a'; getComponent().switchValue = 'a';
detectChangesAndExpectText('when a'); detectChangesAndExpectText('when a');
getComponent().switchValue = 'b'; getComponent().switchValue = 'b';
detectChangesAndExpectText('when default'); detectChangesAndExpectText('when default');
getComponent().switchValue = 'c'; getComponent().switchValue = 'c';
detectChangesAndExpectText('when default'); detectChangesAndExpectText('when default');
})); });
it('should support multiple whens with the same value', async(() => { it('should support multiple whens with the same value', () => {
const template = '<div>' + const template = '<div>' +
'<ul [ngSwitch]="switchValue">' + '<ul [ngSwitch]="switchValue">' +
'<template ngSwitchCase="a"><li>when a1;</li></template>' + '<template ngSwitchCase="a"><li>when a1;</li></template>' +
'<template ngSwitchCase="b"><li>when b1;</li></template>' + '<template ngSwitchCase="b"><li>when b1;</li></template>' +
'<template ngSwitchCase="a"><li>when a2;</li></template>' + '<template ngSwitchCase="a"><li>when a2;</li></template>' +
'<template ngSwitchCase="b"><li>when b2;</li></template>' + '<template ngSwitchCase="b"><li>when b2;</li></template>' +
'<template ngSwitchDefault><li>when default1;</li></template>' + '<template ngSwitchDefault><li>when default1;</li></template>' +
'<template ngSwitchDefault><li>when default2;</li></template>' + '<template ngSwitchDefault><li>when default2;</li></template>' +
'</ul></div>'; '</ul></div>';
fixture = createTestComponent(template); fixture = createTestComponent(template);
detectChangesAndExpectText('when default1;when default2;'); detectChangesAndExpectText('when default1;when default2;');
getComponent().switchValue = 'a'; getComponent().switchValue = 'a';
detectChangesAndExpectText('when a1;when a2;'); detectChangesAndExpectText('when a1;when a2;');
getComponent().switchValue = 'b'; getComponent().switchValue = 'b';
detectChangesAndExpectText('when b1;when b2;'); detectChangesAndExpectText('when b1;when b2;');
})); });
}); });
describe('when values changes', () => { describe('when values changes', () => {
it('should switch amongst when values', async(() => { it('should switch amongst when values', () => {
const template = '<div>' + const template = '<div>' +
'<ul [ngSwitch]="switchValue">' + '<ul [ngSwitch]="switchValue">' +
'<template [ngSwitchCase]="when1"><li>when 1;</li></template>' + '<template [ngSwitchCase]="when1"><li>when 1;</li></template>' +
'<template [ngSwitchCase]="when2"><li>when 2;</li></template>' + '<template [ngSwitchCase]="when2"><li>when 2;</li></template>' +
'<template ngSwitchDefault><li>when default;</li></template>' + '<template ngSwitchDefault><li>when default;</li></template>' +
'</ul></div>'; '</ul></div>';
fixture = createTestComponent(template); fixture = createTestComponent(template);
getComponent().when1 = 'a'; getComponent().when1 = 'a';
getComponent().when2 = 'b'; getComponent().when2 = 'b';
getComponent().switchValue = 'a'; getComponent().switchValue = 'a';
detectChangesAndExpectText('when 1;'); detectChangesAndExpectText('when 1;');
getComponent().switchValue = 'b'; getComponent().switchValue = 'b';
detectChangesAndExpectText('when 2;'); detectChangesAndExpectText('when 2;');
getComponent().switchValue = 'c'; getComponent().switchValue = 'c';
detectChangesAndExpectText('when default;'); detectChangesAndExpectText('when default;');
getComponent().when1 = 'c'; getComponent().when1 = 'c';
detectChangesAndExpectText('when 1;'); detectChangesAndExpectText('when 1;');
getComponent().when1 = 'd'; getComponent().when1 = 'd';
detectChangesAndExpectText('when default;'); detectChangesAndExpectText('when default;');
})); });
});
describe('corner cases', () => {
it('should not create the default case if another case matches', () => {
const log: string[] = [];
@Directive({selector: '[test]'})
class TestDirective {
constructor(@Attribute('test') test: string) { log.push(test); }
}
const template = '<div [ngSwitch]="switchValue">' +
'<div *ngSwitchCase="\'a\'" test="aCase"></div>' +
'<div *ngSwitchDefault test="defaultCase"></div>' +
'</div>';
TestBed.configureTestingModule({declarations: [TestDirective]});
TestBed.overrideComponent(TestComponent, {set: {template: template}})
.createComponent(TestComponent);
const fixture = TestBed.createComponent(TestComponent);
fixture.componentInstance.switchValue = 'a';
fixture.detectChanges();
expect(log).toEqual(['aCase']);
});
it('should create the default case if there is no other case', () => {
const template = '<div>' +
'<ul [ngSwitch]="switchValue">' +
'<template ngSwitchDefault><li>when default1;</li></template>' +
'<template ngSwitchDefault><li>when default2;</li></template>' +
'</ul></div>';
fixture = createTestComponent(template);
detectChangesAndExpectText('when default1;when default2;');
});
it('should allow defaults before cases', () => {
const template = '<div>' +
'<ul [ngSwitch]="switchValue">' +
'<template ngSwitchDefault><li>when default1;</li></template>' +
'<template ngSwitchDefault><li>when default2;</li></template>' +
'<template ngSwitchCase="a"><li>when a1;</li></template>' +
'<template ngSwitchCase="b"><li>when b1;</li></template>' +
'<template ngSwitchCase="a"><li>when a2;</li></template>' +
'<template ngSwitchCase="b"><li>when b2;</li></template>' +
'</ul></div>';
fixture = createTestComponent(template);
detectChangesAndExpectText('when default1;when default2;');
getComponent().switchValue = 'a';
detectChangesAndExpectText('when a1;when a2;');
getComponent().switchValue = 'b';
detectChangesAndExpectText('when b1;when b2;');
});
}); });
}); });
} }

View File

@ -166,14 +166,15 @@ export declare class NgSwitch {
} }
/** @stable */ /** @stable */
export declare class NgSwitchCase { export declare class NgSwitchCase implements DoCheck {
ngSwitchCase: any; ngSwitchCase: any;
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, ngSwitch: NgSwitch); constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, ngSwitch: NgSwitch);
ngDoCheck(): void;
} }
/** @stable */ /** @stable */
export declare class NgSwitchDefault { export declare class NgSwitchDefault {
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, sswitch: NgSwitch); constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, ngSwitch: NgSwitch);
} }
/** @experimental */ /** @experimental */