diff --git a/src/lib/tabs/public-api.ts b/src/lib/tabs/public-api.ts index 9d08a2fef02..5bdae581689 100644 --- a/src/lib/tabs/public-api.ts +++ b/src/lib/tabs/public-api.ts @@ -9,11 +9,14 @@ export * from './tabs-module'; export * from './tab-group'; export {MatInkBar} from './ink-bar'; -export {MatTabBody, MatTabBodyOriginState, MatTabBodyPositionState} from './tab-body'; +export { + MatTabBody, + MatTabBodyOriginState, + MatTabBodyPositionState, + MatTabBodyPortal +} from './tab-body'; export {MatTabHeader, ScrollDirection} from './tab-header'; export {MatTabLabelWrapper} from './tab-label-wrapper'; export {MatTab} from './tab'; export {MatTabLabel} from './tab-label'; export {MatTabNav, MatTabLink} from './tab-nav-bar/index'; - - diff --git a/src/lib/tabs/tab-body.html b/src/lib/tabs/tab-body.html index 0650b3fdc08..312b1848a07 100644 --- a/src/lib/tabs/tab-body.html +++ b/src/lib/tabs/tab-body.html @@ -2,5 +2,5 @@ [@translateTab]="_position" (@translateTab.start)="_onTranslateTabStarted($event)" (@translateTab.done)="_onTranslateTabComplete($event)"> - + diff --git a/src/lib/tabs/tab-body.spec.ts b/src/lib/tabs/tab-body.spec.ts index 4572e9f756f..58c9f81edbc 100644 --- a/src/lib/tabs/tab-body.spec.ts +++ b/src/lib/tabs/tab-body.spec.ts @@ -2,10 +2,10 @@ import {Direction, Directionality} from '@angular/cdk/bidi'; import {PortalModule, TemplatePortal} from '@angular/cdk/portal'; import {CommonModule} from '@angular/common'; import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; -import {async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {MatRippleModule} from '@angular/material/core'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MatTabBody} from './tab-body'; +import {MatTabBody, MatTabBodyPortal} from './tab-body'; describe('MatTabBody', () => { @@ -17,6 +17,7 @@ describe('MatTabBody', () => { imports: [CommonModule, PortalModule, MatRippleModule, NoopAnimationsModule], declarations: [ MatTabBody, + MatTabBodyPortal, SimpleTabBodyApp, ], providers: [ @@ -145,30 +146,6 @@ describe('MatTabBody', () => { expect(fixture.componentInstance.tabBody._position).toBe('left'); }); }); - - describe('on centered', () => { - let fixture: ComponentFixture; - - beforeEach(fakeAsync(() => { - fixture = TestBed.createComponent(SimpleTabBodyApp); - })); - - it('should attach the content when centered and detach when not', fakeAsync(() => { - fixture.componentInstance.position = 1; - fixture.detectChanges(); - expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false); - - fixture.componentInstance.position = 0; - fixture.detectChanges(); - expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(true); - - fixture.componentInstance.position = 1; - fixture.detectChanges(); - flushMicrotasks(); // Finish animation and let it detach in animation done handler - expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false); - })); - }); - }); diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index a0519883e43..e08eb716b14 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -7,17 +7,21 @@ */ import { - ViewChild, Component, Input, + Inject, Output, EventEmitter, + OnDestroy, OnInit, ElementRef, + Directive, Optional, - AfterViewChecked, ViewEncapsulation, ChangeDetectionStrategy, + ComponentFactoryResolver, + ViewContainerRef, + forwardRef, } from '@angular/core'; import { trigger, @@ -29,7 +33,10 @@ import { } from '@angular/animations'; import {TemplatePortal, CdkPortalOutlet} from '@angular/cdk/portal'; import {Directionality, Direction} from '@angular/cdk/bidi'; +import {Subscription} from 'rxjs/Subscription'; +/** Workaround for https://github.com/angular/angular/issues/17849 */ +export const _MatTabBodyPortalBaseClass = CdkPortalOutlet; /** * These position states are used internally as animation states for the tab body. Setting the @@ -52,6 +59,44 @@ export type MatTabBodyPositionState = */ export type MatTabBodyOriginState = 'left' | 'right'; +/** + * The portal host directive for the contents of the tab. + * @docs-private + */ +@Directive({ + selector: '[matTabBodyHost]' +}) +export class MatTabBodyPortal extends _MatTabBodyPortalBaseClass implements OnInit, OnDestroy { + /** A subscription to events for when the tab body begins centering. */ + private _centeringSub: Subscription; + + constructor( + _componentFactoryResolver: ComponentFactoryResolver, + _viewContainerRef: ViewContainerRef, + @Inject(forwardRef(() => MatTabBody)) private _host: MatTabBody) { + super(_componentFactoryResolver, _viewContainerRef); + } + + /** Set initial visibility or set up subscription for changing visibility. */ + ngOnInit(): void { + if (this._host._isCenterPosition(this._host._position)) { + this.attach(this._host._content); + } else { + this._centeringSub = this._host._beforeCentering.subscribe(() => { + this.attach(this._host._content); + this._centeringSub.unsubscribe(); + }); + } + } + + /** Clean up subscription if necessary. */ + ngOnDestroy(): void { + if (this._centeringSub && !this._centeringSub.closed) { + this._centeringSub.unsubscribe(); + } + } +} + /** * Wrapper for the contents of a tab. * @docs-private @@ -86,13 +131,13 @@ export type MatTabBodyOriginState = 'left' | 'right'; ]) ] }) -export class MatTabBody implements OnInit, AfterViewChecked { - /** The portal outlet inside of this container into which the tab body content will be loaded. */ - @ViewChild(CdkPortalOutlet) _portalOutlet: CdkPortalOutlet; - +export class MatTabBody implements OnInit { /** Event emitted when the tab begins to animate towards the center as the active tab. */ @Output() _onCentering: EventEmitter = new EventEmitter(); + /** Event emitted before the centering of the tab begins. */ + @Output() _beforeCentering: EventEmitter = new EventEmitter(); + /** Event emitted when the tab completes its animation towards the center. */ @Output() _onCentered: EventEmitter = new EventEmitter(true); @@ -139,28 +184,14 @@ export class MatTabBody implements OnInit, AfterViewChecked { } } - /** - * After the view has been set, check if the tab content is set to the center and attach the - * content if it is not already attached. - */ - ngAfterViewChecked() { - if (this._isCenterPosition(this._position) && !this._portalOutlet.hasAttached()) { - this._portalOutlet.attach(this._content); - } - } - - _onTranslateTabStarted(e: AnimationEvent) { + _onTranslateTabStarted(e: AnimationEvent): void { if (this._isCenterPosition(e.toState)) { + this._beforeCentering.emit(); this._onCentering.emit(this._elementRef.nativeElement.clientHeight); } } - _onTranslateTabComplete(e: AnimationEvent) { - // If the end state is that the tab is not centered, then detach the content. - if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) { - this._portalOutlet.detach(); - } - + _onTranslateTabComplete(e: AnimationEvent): void { // If the transition to the center is complete, emit an event. if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) { this._onCentered.emit(); @@ -173,7 +204,7 @@ export class MatTabBody implements OnInit, AfterViewChecked { } /** Whether the provided position state is considered center, regardless of origin. */ - private _isCenterPosition(position: MatTabBodyPositionState|string): boolean { + _isCenterPosition(position: MatTabBodyPositionState|string): boolean { return position == 'center' || position == 'left-origin-center' || position == 'right-origin-center'; diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index 5134ee02467..03780b818d2 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -26,15 +26,22 @@ describe('MatTabGroup', () => { describe('basic behavior', () => { let fixture: ComponentFixture; + let element: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(SimpleTabsTestApp); + element = fixture.nativeElement; }); it('should default to the first tab', () => { checkSelectedIndex(1, fixture); }); + it('will properly load content on first change detection pass', () => { + fixture.detectChanges(); + expect(element.querySelectorAll('.mat-tab-body')[1].querySelectorAll('span').length).toBe(3); + }); + it('should change selected index on click', () => { let component = fixture.debugElement.componentInstance; component.selectedIndex = 0; @@ -318,23 +325,26 @@ describe('MatTabGroup', () => { fixture.debugElement.query(By.directive(MatTabGroup)).componentInstance as MatTabGroup; }); - it('should support a tab-group with the simple api', () => { + it('should support a tab-group with the simple api', async(() => { expect(getSelectedLabel(fixture).textContent).toMatch('Junk food'); expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries'); tabGroup.selectedIndex = 2; fixture.detectChanges(); + // Use whenStable to wait for async observables and change detection to run in content. + fixture.whenStable().then(() => { - expect(getSelectedLabel(fixture).textContent).toMatch('Fruit'); - expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes'); + expect(getSelectedLabel(fixture).textContent).toMatch('Fruit'); + expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes'); - fixture.componentInstance.otherLabel = 'Chips'; - fixture.componentInstance.otherContent = 'Salt, vinegar'; - fixture.detectChanges(); + fixture.componentInstance.otherLabel = 'Chips'; + fixture.componentInstance.otherContent = 'Salt, vinegar'; + fixture.detectChanges(); - expect(getSelectedLabel(fixture).textContent).toMatch('Chips'); - expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar'); - }); + expect(getSelectedLabel(fixture).textContent).toMatch('Chips'); + expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar'); + }); + })); it('should support @ViewChild in the tab content', () => { expect(fixture.componentInstance.legumes).toBeTruthy(); @@ -415,7 +425,7 @@ describe('nested MatTabGroup with enabled animations', () => { Tab Two - Tab two content + Tab twocontent Tab Three diff --git a/src/lib/tabs/tabs-module.ts b/src/lib/tabs/tabs-module.ts index 40e4e962cbf..5defcedd2e5 100644 --- a/src/lib/tabs/tabs-module.ts +++ b/src/lib/tabs/tabs-module.ts @@ -14,7 +14,7 @@ import {NgModule} from '@angular/core'; import {MatCommonModule, MatRippleModule} from '@angular/material/core'; import {MatInkBar} from './ink-bar'; import {MatTab} from './tab'; -import {MatTabBody} from './tab-body'; +import {MatTabBody, MatTabBodyPortal} from './tab-body'; import {MatTabGroup} from './tab-group'; import {MatTabHeader} from './tab-header'; import {MatTabLabel} from './tab-label'; @@ -49,6 +49,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar'; MatTabNav, MatTabLink, MatTabBody, + MatTabBodyPortal, MatTabHeader ], providers: [VIEWPORT_RULER_PROVIDER],