Skip to content

Commit a43358a

Browse files
committed
Move tabs to smart portal directive.
1 parent 3571f68 commit a43358a

File tree

7 files changed

+134
-38
lines changed

7 files changed

+134
-38
lines changed

src/lib/tabs/public_api.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
export * from './tabs-module';
1010
export * from './tab-group';
1111
export {MdInkBar} from './ink-bar';
12-
export {MdTabBody, MdTabBodyOriginState, MdTabBodyPositionState} from './tab-body';
12+
export {
13+
MdTabBody,
14+
MdTabBodyOriginState,
15+
MdTabBodyPositionState,
16+
MdTabBodyPortal
17+
} from './tab-body';
1318
export {MdTabHeader, ScrollDirection} from './tab-header';
1419
export {MdTabLabelWrapper} from './tab-label-wrapper';
1520
export {MdTab} from './tab';

src/lib/tabs/tab-body.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<div class="mat-tab-body-content" #content
2+
[style.display]="_portalHost?.visible ? 'block' : 'none'"
23
[@translateTab]="_position"
34
(@translateTab.start)="_onTranslateTabStarted($event)"
45
(@translateTab.done)="_onTranslateTabComplete($event)">
5-
<ng-template cdkPortalHost></ng-template>
6+
<ng-template matTabBodyHost></ng-template>
67
</div>

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

Lines changed: 6 additions & 8 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, fakeAsync, tick, TestBed} from '@angular/core/testing';
66
import {MdRippleModule} from '@angular/material/core';
77
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
8-
import {MdTabBody} from './tab-body';
8+
import {MdTabBody, MdTabBodyPortal} from './tab-body';
99

1010

1111
describe('MdTabBody', () => {
@@ -17,6 +17,7 @@ describe('MdTabBody', () => {
1717
imports: [CommonModule, PortalModule, MdRippleModule, NoopAnimationsModule],
1818
declarations: [
1919
MdTabBody,
20+
MdTabBodyPortal,
2021
SimpleTabBodyApp,
2122
],
2223
providers: [
@@ -153,19 +154,16 @@ describe('MdTabBody', () => {
153154
fixture = TestBed.createComponent(SimpleTabBodyApp);
154155
}));
155156

156-
it('should attach the content when centered and detach when not', fakeAsync(() => {
157+
it('should attach when first made visible', fakeAsync(() => {
157158
fixture.componentInstance.position = 1;
158159
fixture.detectChanges();
160+
tick(1000);
159161
expect(fixture.componentInstance.mdTabBody._portalHost.hasAttached()).toBe(false);
160162

161163
fixture.componentInstance.position = 0;
162164
fixture.detectChanges();
165+
tick(1000);
163166
expect(fixture.componentInstance.mdTabBody._portalHost.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.mdTabBody._portalHost.hasAttached()).toBe(false);
169167
}));
170168
});
171169

src/lib/tabs/tab-body.ts

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import {
1414
EventEmitter,
1515
OnInit,
1616
ElementRef,
17+
Directive,
1718
Optional,
18-
AfterViewChecked,
1919
ViewEncapsulation,
2020
ChangeDetectionStrategy,
21+
ComponentFactoryResolver,
22+
ViewContainerRef,
2123
} from '@angular/core';
2224
import {
2325
trigger,
@@ -29,7 +31,10 @@ import {
2931
} from '@angular/animations';
3032
import {TemplatePortal, PortalHostDirective} from '@angular/cdk/portal';
3133
import {Directionality, Direction} from '@angular/cdk/bidi';
34+
import {Subscription} from 'rxjs/Subscription';
3235

36+
/** Workaround for https://github.com/angular/angular/issues/17849 */
37+
export const _MdTabBodyPortalBaseClass = PortalHostDirective;
3338

3439
/**
3540
* These position states are used internally as animation states for the tab body. Setting the
@@ -52,6 +57,53 @@ export type MdTabBodyPositionState =
5257
*/
5358
export type MdTabBodyOriginState = 'left' | 'right';
5459

60+
/**
61+
* The portal host directive for the contents of the tab.
62+
* @docs-private
63+
*/
64+
@Directive({
65+
selector: '[matTabBodyHost]'
66+
})
67+
export class MdTabBodyPortal extends _MdTabBodyPortalBaseClass implements OnInit {
68+
/** Whether the tab body should be visible. */
69+
visible = false;
70+
/** A subscription to events for when the tab body begins centering. */
71+
centeringSub: Subscription;
72+
/** A subscription to events for when the tab body completes leaving from center. */
73+
leavingCenterSub: Subscription;
74+
75+
constructor(
76+
_componentFactoryResolver: ComponentFactoryResolver,
77+
_viewContainerRef: ViewContainerRef,
78+
private host: MdTabBody) {
79+
super(_componentFactoryResolver, _viewContainerRef);
80+
}
81+
82+
/** Set up subscriptions for changing visibility and set initial visibility. */
83+
ngOnInit(): void {
84+
this.centeringSub = this.host.onCentering.subscribe(() => this.setVisibility(true));
85+
this.leavingCenterSub = this.host._afterLeavingCenter.subscribe(
86+
() => this.setVisibility(false));
87+
if (this.host._isCenterPosition(this.host._position)) {
88+
this.setVisibility(true);
89+
}
90+
}
91+
92+
/** Clean up subscriptions. */
93+
ngOnDestroy(): void {
94+
this.leavingCenterSub.unsubscribe();
95+
this.centeringSub.unsubscribe();
96+
}
97+
98+
/** Set the visiblity of the tab body content, attaching the content if necessary. */
99+
setVisibility(visible: boolean): void {
100+
if (visible && !this.hasAttached()) {
101+
this.attach(this.host._content);
102+
}
103+
this.visible = visible;
104+
}
105+
}
106+
55107
/**
56108
* Wrapper for the contents of a tab.
57109
* @docs-private
@@ -88,13 +140,16 @@ export type MdTabBodyOriginState = 'left' | 'right';
88140
])
89141
]
90142
})
91-
export class MdTabBody implements OnInit, AfterViewChecked {
143+
export class MdTabBody implements OnInit {
92144
/** The portal host inside of this container into which the tab body content will be loaded. */
93-
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
145+
@ViewChild(MdTabBodyPortal) _portalHost: MdTabBodyPortal;
94146

95147
/** Event emitted when the tab begins to animate towards the center as the active tab. */
96148
@Output() onCentering: EventEmitter<number> = new EventEmitter<number>();
97149

150+
/** Event emitted when the tab completes its animation away from the center. */
151+
@Output() _afterLeavingCenter: EventEmitter<void> = new EventEmitter<void>();
152+
98153
/** Event emitted when the tab completes its animation towards the center. */
99154
@Output() onCentered: EventEmitter<void> = new EventEmitter<void>(true);
100155

@@ -141,26 +196,16 @@ export class MdTabBody implements OnInit, AfterViewChecked {
141196
}
142197
}
143198

144-
/**
145-
* After the view has been set, check if the tab content is set to the center and attach the
146-
* content if it is not already attached.
147-
*/
148-
ngAfterViewChecked() {
149-
if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) {
150-
this._portalHost.attach(this._content);
151-
}
152-
}
153-
154-
_onTranslateTabStarted(e: AnimationEvent) {
199+
_onTranslateTabStarted(e: AnimationEvent): void {
155200
if (this._isCenterPosition(e.toState)) {
156201
this.onCentering.emit(this._elementRef.nativeElement.clientHeight);
157202
}
158203
}
159204

160-
_onTranslateTabComplete(e: AnimationEvent) {
161-
// If the end state is that the tab is not centered, then detach the content.
205+
_onTranslateTabComplete(e: AnimationEvent): void {
206+
// If the end state is that the tab is not centered, emit an event.
162207
if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) {
163-
this._portalHost.detach();
208+
this._afterLeavingCenter.emit();
164209
}
165210

166211
// If the transition to the center is complete, emit an event.
@@ -175,7 +220,7 @@ export class MdTabBody implements OnInit, AfterViewChecked {
175220
}
176221

177222
/** Whether the provided position state is considered center, regardless of origin. */
178-
private _isCenterPosition(position: MdTabBodyPositionState|string): boolean {
223+
_isCenterPosition(position: MdTabBodyPositionState|string): boolean {
179224
return position == 'center' ||
180225
position == 'left-origin-center' ||
181226
position == 'right-origin-center';

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -322,23 +322,25 @@ describe('MdTabGroup', () => {
322322
fixture.debugElement.query(By.directive(MdTabGroup)).componentInstance as MdTabGroup;
323323
});
324324

325-
it('should support a tab-group with the simple api', () => {
325+
it('should support a tab-group with the simple api', async(() => {
326326
expect(getSelectedLabel(fixture).textContent).toMatch('Junk food');
327327
expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries');
328328

329329
tabGroup.selectedIndex = 2;
330330
fixture.detectChanges();
331+
fixture.whenStable().then(() => {
331332

332-
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
333-
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
333+
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
334+
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
334335

335-
fixture.componentInstance.otherLabel = 'Chips';
336-
fixture.componentInstance.otherContent = 'Salt, vinegar';
337-
fixture.detectChanges();
336+
fixture.componentInstance.otherLabel = 'Chips';
337+
fixture.componentInstance.otherContent = 'Salt, vinegar';
338+
fixture.detectChanges();
338339

339-
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
340-
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
341-
});
340+
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
341+
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
342+
});
343+
}));
342344

343345
it('should support @ViewChild in the tab content', () => {
344346
expect(fixture.componentInstance.legumes).toBeTruthy();

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 {MdCommonModule, MdRippleModule} from '@angular/material/core';
1515
import {MdInkBar} from './ink-bar';
1616
import {MdTab} from './tab';
17-
import {MdTabBody} from './tab-body';
17+
import {MdTabBody, MdTabBodyPortal} from './tab-body';
1818
import {MdTabGroup} from './tab-group';
1919
import {MdTabHeader} from './tab-header';
2020
import {MdTabLabel} from './tab-label';
@@ -41,6 +41,7 @@ import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar';
4141
MdTabLink,
4242
],
4343
declarations: [
44+
MdTabBodyPortal,
4445
MdTabGroup,
4546
MdTabLabel,
4647
MdTab,

src/lib/tabs/tabs.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {Component} from '@angular/core';
2+
import {CommonModule} from '@angular/common';
3+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
4+
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
5+
import {MdTabsModule} from './public_api';
6+
7+
8+
describe('MdTabs', () => {
9+
let fixture: ComponentFixture<TestTabs>;
10+
let element: HTMLElement;
11+
12+
beforeEach(async(() => {
13+
TestBed.configureTestingModule({
14+
imports: [CommonModule, MdTabsModule, BrowserAnimationsModule],
15+
declarations: [TestTabs]
16+
});
17+
TestBed.compileComponents();
18+
}));
19+
20+
21+
beforeEach(() => {
22+
fixture = TestBed.createComponent(TestTabs);
23+
element = fixture.nativeElement;
24+
fixture.detectChanges();
25+
});
26+
27+
it('will properly load content on first change detection pass', () => {
28+
expect(element.querySelectorAll('.mat-tab-body')[0].querySelectorAll('.items').length).toBe(3);
29+
});
30+
});
31+
32+
33+
@Component({
34+
template: `
35+
<md-tab-group>
36+
<md-tab label="tab 1">
37+
<p class="items">content for tab 1</p>
38+
<p class="items">content for tab 1</p>
39+
<p class="items">content for tab 1</p>
40+
</md-tab>
41+
</md-tab-group>
42+
`
43+
})
44+
class TestTabs {}

0 commit comments

Comments
 (0)