Skip to content

Commit 0e03bf4

Browse files
chore(tabs): move tabs to smart portal directive. (#7266)
1 parent 691bb73 commit 0e03bf4

File tree

6 files changed

+87
-65
lines changed

6 files changed

+87
-65
lines changed

src/lib/tabs/public-api.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
export * from './tabs-module';
1010
export * from './tab-group';
1111
export {MatInkBar} from './ink-bar';
12-
export {MatTabBody, MatTabBodyOriginState, MatTabBodyPositionState} from './tab-body';
12+
export {
13+
MatTabBody,
14+
MatTabBodyOriginState,
15+
MatTabBodyPositionState,
16+
MatTabBodyPortal
17+
} from './tab-body';
1318
export {MatTabHeader, ScrollDirection} from './tab-header';
1419
export {MatTabLabelWrapper} from './tab-label-wrapper';
1520
export {MatTab} from './tab';
1621
export {MatTabLabel} from './tab-label';
1722
export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
18-
19-

src/lib/tabs/tab-body.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
[@translateTab]="_position"
33
(@translateTab.start)="_onTranslateTabStarted($event)"
44
(@translateTab.done)="_onTranslateTabComplete($event)">
5-
<ng-template cdkPortalOutlet></ng-template>
5+
<ng-template matTabBodyHost></ng-template>
66
</div>

src/lib/tabs/tab-body.spec.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import {Direction, Directionality} from '@angular/cdk/bidi';
22
import {PortalModule, TemplatePortal} from '@angular/cdk/portal';
33
import {CommonModule} from '@angular/common';
44
import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
5-
import {async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
5+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
66
import {MatRippleModule} from '@angular/material/core';
77
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
8-
import {MatTabBody} from './tab-body';
8+
import {MatTabBody, MatTabBodyPortal} from './tab-body';
99

1010

1111
describe('MatTabBody', () => {
@@ -17,6 +17,7 @@ describe('MatTabBody', () => {
1717
imports: [CommonModule, PortalModule, MatRippleModule, NoopAnimationsModule],
1818
declarations: [
1919
MatTabBody,
20+
MatTabBodyPortal,
2021
SimpleTabBodyApp,
2122
],
2223
providers: [
@@ -145,30 +146,6 @@ describe('MatTabBody', () => {
145146
expect(fixture.componentInstance.tabBody._position).toBe('left');
146147
});
147148
});
148-
149-
describe('on centered', () => {
150-
let fixture: ComponentFixture<SimpleTabBodyApp>;
151-
152-
beforeEach(fakeAsync(() => {
153-
fixture = TestBed.createComponent(SimpleTabBodyApp);
154-
}));
155-
156-
it('should attach the content when centered and detach when not', fakeAsync(() => {
157-
fixture.componentInstance.position = 1;
158-
fixture.detectChanges();
159-
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false);
160-
161-
fixture.componentInstance.position = 0;
162-
fixture.detectChanges();
163-
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(true);
164-
165-
fixture.componentInstance.position = 1;
166-
fixture.detectChanges();
167-
flushMicrotasks(); // Finish animation and let it detach in animation done handler
168-
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false);
169-
}));
170-
});
171-
172149
});
173150

174151

src/lib/tabs/tab-body.ts

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77
*/
88

99
import {
10-
ViewChild,
1110
Component,
1211
Input,
12+
Inject,
1313
Output,
1414
EventEmitter,
15+
OnDestroy,
1516
OnInit,
1617
ElementRef,
18+
Directive,
1719
Optional,
18-
AfterViewChecked,
1920
ViewEncapsulation,
2021
ChangeDetectionStrategy,
22+
ComponentFactoryResolver,
23+
ViewContainerRef,
24+
forwardRef,
2125
} from '@angular/core';
2226
import {
2327
trigger,
@@ -29,7 +33,10 @@ import {
2933
} from '@angular/animations';
3034
import {TemplatePortal, CdkPortalOutlet} from '@angular/cdk/portal';
3135
import {Directionality, Direction} from '@angular/cdk/bidi';
36+
import {Subscription} from 'rxjs/Subscription';
3237

38+
/** Workaround for https://github.com/angular/angular/issues/17849 */
39+
export const _MatTabBodyPortalBaseClass = CdkPortalOutlet;
3340

3441
/**
3542
* These position states are used internally as animation states for the tab body. Setting the
@@ -52,6 +59,44 @@ export type MatTabBodyPositionState =
5259
*/
5360
export type MatTabBodyOriginState = 'left' | 'right';
5461

62+
/**
63+
* The portal host directive for the contents of the tab.
64+
* @docs-private
65+
*/
66+
@Directive({
67+
selector: '[matTabBodyHost]'
68+
})
69+
export class MatTabBodyPortal extends _MatTabBodyPortalBaseClass implements OnInit, OnDestroy {
70+
/** A subscription to events for when the tab body begins centering. */
71+
private _centeringSub: Subscription;
72+
73+
constructor(
74+
_componentFactoryResolver: ComponentFactoryResolver,
75+
_viewContainerRef: ViewContainerRef,
76+
@Inject(forwardRef(() => MatTabBody)) private _host: MatTabBody) {
77+
super(_componentFactoryResolver, _viewContainerRef);
78+
}
79+
80+
/** Set initial visibility or set up subscription for changing visibility. */
81+
ngOnInit(): void {
82+
if (this._host._isCenterPosition(this._host._position)) {
83+
this.attach(this._host._content);
84+
} else {
85+
this._centeringSub = this._host._beforeCentering.subscribe(() => {
86+
this.attach(this._host._content);
87+
this._centeringSub.unsubscribe();
88+
});
89+
}
90+
}
91+
92+
/** Clean up subscription if necessary. */
93+
ngOnDestroy(): void {
94+
if (this._centeringSub && !this._centeringSub.closed) {
95+
this._centeringSub.unsubscribe();
96+
}
97+
}
98+
}
99+
55100
/**
56101
* Wrapper for the contents of a tab.
57102
* @docs-private
@@ -86,13 +131,13 @@ export type MatTabBodyOriginState = 'left' | 'right';
86131
])
87132
]
88133
})
89-
export class MatTabBody implements OnInit, AfterViewChecked {
90-
/** The portal outlet inside of this container into which the tab body content will be loaded. */
91-
@ViewChild(CdkPortalOutlet) _portalOutlet: CdkPortalOutlet;
92-
134+
export class MatTabBody implements OnInit {
93135
/** Event emitted when the tab begins to animate towards the center as the active tab. */
94136
@Output() _onCentering: EventEmitter<number> = new EventEmitter<number>();
95137

138+
/** Event emitted before the centering of the tab begins. */
139+
@Output() _beforeCentering: EventEmitter<number> = new EventEmitter<number>();
140+
96141
/** Event emitted when the tab completes its animation towards the center. */
97142
@Output() _onCentered: EventEmitter<void> = new EventEmitter<void>(true);
98143

@@ -139,28 +184,14 @@ export class MatTabBody implements OnInit, AfterViewChecked {
139184
}
140185
}
141186

142-
/**
143-
* After the view has been set, check if the tab content is set to the center and attach the
144-
* content if it is not already attached.
145-
*/
146-
ngAfterViewChecked() {
147-
if (this._isCenterPosition(this._position) && !this._portalOutlet.hasAttached()) {
148-
this._portalOutlet.attach(this._content);
149-
}
150-
}
151-
152-
_onTranslateTabStarted(e: AnimationEvent) {
187+
_onTranslateTabStarted(e: AnimationEvent): void {
153188
if (this._isCenterPosition(e.toState)) {
189+
this._beforeCentering.emit();
154190
this._onCentering.emit(this._elementRef.nativeElement.clientHeight);
155191
}
156192
}
157193

158-
_onTranslateTabComplete(e: AnimationEvent) {
159-
// If the end state is that the tab is not centered, then detach the content.
160-
if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) {
161-
this._portalOutlet.detach();
162-
}
163-
194+
_onTranslateTabComplete(e: AnimationEvent): void {
164195
// If the transition to the center is complete, emit an event.
165196
if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) {
166197
this._onCentered.emit();
@@ -173,7 +204,7 @@ export class MatTabBody implements OnInit, AfterViewChecked {
173204
}
174205

175206
/** Whether the provided position state is considered center, regardless of origin. */
176-
private _isCenterPosition(position: MatTabBodyPositionState|string): boolean {
207+
_isCenterPosition(position: MatTabBodyPositionState|string): boolean {
177208
return position == 'center' ||
178209
position == 'left-origin-center' ||
179210
position == 'right-origin-center';

src/lib/tabs/tab-group.spec.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,22 @@ describe('MatTabGroup', () => {
2626

2727
describe('basic behavior', () => {
2828
let fixture: ComponentFixture<SimpleTabsTestApp>;
29+
let element: HTMLElement;
2930

3031
beforeEach(() => {
3132
fixture = TestBed.createComponent(SimpleTabsTestApp);
33+
element = fixture.nativeElement;
3234
});
3335

3436
it('should default to the first tab', () => {
3537
checkSelectedIndex(1, fixture);
3638
});
3739

40+
it('will properly load content on first change detection pass', () => {
41+
fixture.detectChanges();
42+
expect(element.querySelectorAll('.mat-tab-body')[1].querySelectorAll('span').length).toBe(3);
43+
});
44+
3845
it('should change selected index on click', () => {
3946
let component = fixture.debugElement.componentInstance;
4047
component.selectedIndex = 0;
@@ -318,23 +325,26 @@ describe('MatTabGroup', () => {
318325
fixture.debugElement.query(By.directive(MatTabGroup)).componentInstance as MatTabGroup;
319326
});
320327

321-
it('should support a tab-group with the simple api', () => {
328+
it('should support a tab-group with the simple api', async(() => {
322329
expect(getSelectedLabel(fixture).textContent).toMatch('Junk food');
323330
expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries');
324331

325332
tabGroup.selectedIndex = 2;
326333
fixture.detectChanges();
334+
// Use whenStable to wait for async observables and change detection to run in content.
335+
fixture.whenStable().then(() => {
327336

328-
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
329-
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
337+
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
338+
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
330339

331-
fixture.componentInstance.otherLabel = 'Chips';
332-
fixture.componentInstance.otherContent = 'Salt, vinegar';
333-
fixture.detectChanges();
340+
fixture.componentInstance.otherLabel = 'Chips';
341+
fixture.componentInstance.otherContent = 'Salt, vinegar';
342+
fixture.detectChanges();
334343

335-
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
336-
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
337-
});
344+
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
345+
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
346+
});
347+
}));
338348

339349
it('should support @ViewChild in the tab content', () => {
340350
expect(fixture.componentInstance.legumes).toBeTruthy();
@@ -415,7 +425,7 @@ describe('nested MatTabGroup with enabled animations', () => {
415425
</mat-tab>
416426
<mat-tab>
417427
<ng-template mat-tab-label>Tab Two</ng-template>
418-
Tab two content
428+
<span>Tab </span><span>two</span><span>content</span>
419429
</mat-tab>
420430
<mat-tab>
421431
<ng-template mat-tab-label>Tab Three</ng-template>

src/lib/tabs/tabs-module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {NgModule} from '@angular/core';
1414
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
1515
import {MatInkBar} from './ink-bar';
1616
import {MatTab} from './tab';
17-
import {MatTabBody} from './tab-body';
17+
import {MatTabBody, MatTabBodyPortal} from './tab-body';
1818
import {MatTabGroup} from './tab-group';
1919
import {MatTabHeader} from './tab-header';
2020
import {MatTabLabel} from './tab-label';
@@ -49,6 +49,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
4949
MatTabNav,
5050
MatTabLink,
5151
MatTabBody,
52+
MatTabBodyPortal,
5253
MatTabHeader
5354
],
5455
providers: [VIEWPORT_RULER_PROVIDER],

0 commit comments

Comments
 (0)