diff --git a/modules/@angular/common/src/directives/ng_switch.ts b/modules/@angular/common/src/directives/ng_switch.ts index b75870e82b..033e5a8d62 100644 --- a/modules/@angular/common/src/directives/ng_switch.ts +++ b/modules/@angular/common/src/directives/ng_switch.ts @@ -6,19 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; - -import {ListWrapper} from '../facade/collection'; - -const _CASE_DEFAULT = {}; +import {Directive, DoCheck, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; export class SwitchView { + private _created = false; + constructor( private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef) {} - 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]'}) export class NgSwitch { - private _switchValue: any; - private _useDefault: boolean = false; - private _valueViews = new Map(); - private _activeViews: SwitchView[] = []; + private _defaultViews: SwitchView[]; + private _defaultUsed = false; + private _caseCount = 0; + private _lastCaseCheckIndex = 0; + private _lastCasesMatched = false; + private _ngSwitch: any; @Input() - set ngSwitch(value: any) { - // Set of views to display for this value - let views = this._valueViews.get(value); - - 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; + set ngSwitch(newValue: any) { + this._ngSwitch = newValue; + if (this._caseCount === 0) { + this._updateDefaultCases(true); } } /** @internal */ - _registerView(value: any, view: SwitchView): void { - let views = this._valueViews.get(value); - if (!views) { - views = []; - this._valueViews.set(value, views); + _addCase(): number { return this._caseCount++; } + + /** @internal */ + _addDefault(view: SwitchView) { + if (!this._defaultViews) { + this._defaultViews = []; } - views.push(view); + this._defaultViews.push(view); } - private _deregisterView(value: any, view: SwitchView): void { - // `_CASE_DEFAULT` is used a marker for non-registered cases - if (value === _CASE_DEFAULT) return; - const views = this._valueViews.get(value); - if (views.length == 1) { - this._valueViews.delete(value); - } else { - ListWrapper.remove(views, view); + /** @internal */ + _matchCase(value: any): boolean { + const matched = value == this._ngSwitch; + this._lastCasesMatched = this._lastCasesMatched || matched; + this._lastCaseCheckIndex++; + if (this._lastCaseCheckIndex === this._caseCount) { + this._updateDefaultCases(!this._lastCasesMatched); + 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 */ @Directive({selector: '[ngSwitchCase]'}) -export class NgSwitchCase { - // `_CASE_DEFAULT` is used as a marker for a not yet initialized value - private _value: any = _CASE_DEFAULT; +export class NgSwitchCase implements DoCheck { private _view: SwitchView; - private _switch: NgSwitch; + + @Input() + ngSwitchCase: any; constructor( viewContainer: ViewContainerRef, templateRef: TemplateRef, - @Host() ngSwitch: NgSwitch) { - this._switch = ngSwitch; + @Host() private ngSwitch: NgSwitch) { + ngSwitch._addCase(); this._view = new SwitchView(viewContainer, templateRef); } - @Input() - set ngSwitchCase(value: any) { - this._switch._onCaseValueChanged(this._value, value, this._view); - this._value = value; - } + ngDoCheck() { this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase)); } } /** @@ -226,7 +194,7 @@ export class NgSwitchCase { export class NgSwitchDefault { constructor( viewContainer: ViewContainerRef, templateRef: TemplateRef, - @Host() sswitch: NgSwitch) { - sswitch._registerView(_CASE_DEFAULT, new SwitchView(viewContainer, templateRef)); + @Host() ngSwitch: NgSwitch) { + ngSwitch._addDefault(new SwitchView(viewContainer, templateRef)); } } diff --git a/modules/@angular/common/test/directives/ng_switch_spec.ts b/modules/@angular/common/test/directives/ng_switch_spec.ts index 25011460b9..3030cc4d93 100644 --- a/modules/@angular/common/test/directives/ng_switch_spec.ts +++ b/modules/@angular/common/test/directives/ng_switch_spec.ts @@ -7,8 +7,8 @@ */ import {CommonModule} from '@angular/common'; -import {Component} from '@angular/core'; -import {ComponentFixture, TestBed, async} from '@angular/core/testing'; +import {Attribute, Component, Directive} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/matchers'; export function main() { @@ -32,93 +32,153 @@ export function main() { }); describe('switch value changes', () => { - it('should switch amongst when values', async(() => { - const template = '
' + - '
    ' + - '' + - '' + - '
'; + it('should switch amongst when values', () => { + const template = '
' + + '
    ' + + '' + + '' + + '
'; - fixture = createTestComponent(template); + fixture = createTestComponent(template); - detectChangesAndExpectText(''); + detectChangesAndExpectText(''); - getComponent().switchValue = 'a'; - detectChangesAndExpectText('when a'); + getComponent().switchValue = 'a'; + detectChangesAndExpectText('when a'); - getComponent().switchValue = 'b'; - detectChangesAndExpectText('when b'); - })); + getComponent().switchValue = 'b'; + detectChangesAndExpectText('when b'); + }); - it('should switch amongst when values with fallback to default', async(() => { - const template = '
' + - '
    ' + - '
  • when a
  • ' + - '
  • when default
  • ' + - '
'; + it('should switch amongst when values with fallback to default', () => { + const template = '
' + + '
    ' + + '
  • when a
  • ' + + '
  • when default
  • ' + + '
'; - fixture = createTestComponent(template); - detectChangesAndExpectText('when default'); + fixture = createTestComponent(template); + detectChangesAndExpectText('when default'); - getComponent().switchValue = 'a'; - detectChangesAndExpectText('when a'); + getComponent().switchValue = 'a'; + detectChangesAndExpectText('when a'); - getComponent().switchValue = 'b'; - detectChangesAndExpectText('when default'); + getComponent().switchValue = 'b'; + detectChangesAndExpectText('when default'); - getComponent().switchValue = 'c'; - detectChangesAndExpectText('when default'); - })); + getComponent().switchValue = 'c'; + detectChangesAndExpectText('when default'); + }); - it('should support multiple whens with the same value', async(() => { - const template = '
' + - '
    ' + - '' + - '' + - '' + - '' + - '' + - '' + - '
'; + it('should support multiple whens with the same value', () => { + const template = '
' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '
'; - fixture = createTestComponent(template); - detectChangesAndExpectText('when default1;when default2;'); + fixture = createTestComponent(template); + detectChangesAndExpectText('when default1;when default2;'); - getComponent().switchValue = 'a'; - detectChangesAndExpectText('when a1;when a2;'); + getComponent().switchValue = 'a'; + detectChangesAndExpectText('when a1;when a2;'); - getComponent().switchValue = 'b'; - detectChangesAndExpectText('when b1;when b2;'); - })); + getComponent().switchValue = 'b'; + detectChangesAndExpectText('when b1;when b2;'); + }); }); describe('when values changes', () => { - it('should switch amongst when values', async(() => { - const template = '
' + - '
    ' + - '' + - '' + - '' + - '
'; + it('should switch amongst when values', () => { + const template = '
' + + '
    ' + + '' + + '' + + '' + + '
'; - fixture = createTestComponent(template); - getComponent().when1 = 'a'; - getComponent().when2 = 'b'; - getComponent().switchValue = 'a'; - detectChangesAndExpectText('when 1;'); + fixture = createTestComponent(template); + getComponent().when1 = 'a'; + getComponent().when2 = 'b'; + getComponent().switchValue = 'a'; + detectChangesAndExpectText('when 1;'); - getComponent().switchValue = 'b'; - detectChangesAndExpectText('when 2;'); + getComponent().switchValue = 'b'; + detectChangesAndExpectText('when 2;'); - getComponent().switchValue = 'c'; - detectChangesAndExpectText('when default;'); + getComponent().switchValue = 'c'; + detectChangesAndExpectText('when default;'); - getComponent().when1 = 'c'; - detectChangesAndExpectText('when 1;'); + getComponent().when1 = 'c'; + detectChangesAndExpectText('when 1;'); - getComponent().when1 = 'd'; - detectChangesAndExpectText('when default;'); - })); + getComponent().when1 = 'd'; + 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 = '
' + + '
' + + '
' + + '
'; + + 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 = '
' + + '
    ' + + '' + + '' + + '
'; + + fixture = createTestComponent(template); + detectChangesAndExpectText('when default1;when default2;'); + + }); + + it('should allow defaults before cases', () => { + const template = '
' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '
'; + + fixture = createTestComponent(template); + detectChangesAndExpectText('when default1;when default2;'); + + getComponent().switchValue = 'a'; + detectChangesAndExpectText('when a1;when a2;'); + + getComponent().switchValue = 'b'; + detectChangesAndExpectText('when b1;when b2;'); + }); }); }); } diff --git a/tools/public_api_guard/common/index.d.ts b/tools/public_api_guard/common/index.d.ts index bdff3ab4af..8c22cce3cf 100644 --- a/tools/public_api_guard/common/index.d.ts +++ b/tools/public_api_guard/common/index.d.ts @@ -166,14 +166,15 @@ export declare class NgSwitch { } /** @stable */ -export declare class NgSwitchCase { +export declare class NgSwitchCase implements DoCheck { ngSwitchCase: any; constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef, ngSwitch: NgSwitch); + ngDoCheck(): void; } /** @stable */ export declare class NgSwitchDefault { - constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef, sswitch: NgSwitch); + constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef, ngSwitch: NgSwitch); } /** @experimental */