Skip to content

chore(tabs): use custom portal template directive for hosting tab body content #7266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/lib/tabs/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


2 changes: 1 addition & 1 deletion src/lib/tabs/tab-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<ng-template cdkPortalOutlet></ng-template>
<ng-template matTabBodyHost></ng-template>
</div>
29 changes: 3 additions & 26 deletions src/lib/tabs/tab-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -17,6 +17,7 @@ describe('MatTabBody', () => {
imports: [CommonModule, PortalModule, MatRippleModule, NoopAnimationsModule],
declarations: [
MatTabBody,
MatTabBodyPortal,
SimpleTabBodyApp,
],
providers: [
Expand Down Expand Up @@ -145,30 +146,6 @@ describe('MatTabBody', () => {
expect(fixture.componentInstance.tabBody._position).toBe('left');
});
});

describe('on centered', () => {
let fixture: ComponentFixture<SimpleTabBodyApp>;

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);
}));
});

});


Expand Down
79 changes: 55 additions & 24 deletions src/lib/tabs/tab-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<number> = new EventEmitter<number>();

/** Event emitted before the centering of the tab begins. */
@Output() _beforeCentering: EventEmitter<number> = new EventEmitter<number>();

/** Event emitted when the tab completes its animation towards the center. */
@Output() _onCentered: EventEmitter<void> = new EventEmitter<void>(true);

Expand Down Expand Up @@ -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();
Expand All @@ -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';
Expand Down
30 changes: 20 additions & 10 deletions src/lib/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,22 @@ describe('MatTabGroup', () => {

describe('basic behavior', () => {
let fixture: ComponentFixture<SimpleTabsTestApp>;
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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -415,7 +425,7 @@ describe('nested MatTabGroup with enabled animations', () => {
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Two</ng-template>
Tab two content
<span>Tab </span><span>two</span><span>content</span>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Three</ng-template>
Expand Down
3 changes: 2 additions & 1 deletion src/lib/tabs/tabs-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -49,6 +49,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
MatTabNav,
MatTabLink,
MatTabBody,
MatTabBodyPortal,
MatTabHeader
],
providers: [VIEWPORT_RULER_PROVIDER],
Expand Down