Skip to content

Commit e8f72d1

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

File tree

8 files changed

+137
-58
lines changed

8 files changed

+137
-58
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+
[class.mat-tab-body-hidden]="!visible"
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.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66
overflow: hidden;
77
}
88
}
9+
10+
.mat-tab-body-hidden {
11+
display: none;
12+
}

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, 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: [
@@ -145,30 +146,6 @@ describe('MdTabBody', () => {
145146
expect(fixture.componentInstance.mdTabBody._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.mdTabBody._portalHost.hasAttached()).toBe(false);
160-
161-
fixture.componentInstance.position = 0;
162-
fixture.detectChanges();
163-
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);
169-
}));
170-
});
171-
172149
});
173150

174151

src/lib/tabs/tab-body.ts

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
Input,
1313
Output,
1414
EventEmitter,
15+
OnDestroy,
1516
OnInit,
1617
ElementRef,
18+
Directive,
1719
Optional,
18-
AfterViewChecked,
1920
ViewEncapsulation,
2021
ChangeDetectionStrategy,
22+
ComponentFactoryResolver,
23+
ViewContainerRef,
2124
} from '@angular/core';
2225
import {
2326
trigger,
@@ -29,7 +32,10 @@ import {
2932
} from '@angular/animations';
3033
import {TemplatePortal, PortalHostDirective} from '@angular/cdk/portal';
3134
import {Directionality, Direction} from '@angular/cdk/bidi';
35+
import {Subscription} from 'rxjs/Subscription';
3236

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

3440
/**
3541
* These position states are used internally as animation states for the tab body. Setting the
@@ -52,6 +58,44 @@ export type MdTabBodyPositionState =
5258
*/
5359
export type MdTabBodyOriginState = 'left' | 'right';
5460

61+
/**
62+
* The portal host directive for the contents of the tab.
63+
* @docs-private
64+
*/
65+
@Directive({
66+
selector: '[matTabBodyHost]'
67+
})
68+
export class MdTabBodyPortal extends _MdTabBodyPortalBaseClass implements OnInit, OnDestroy {
69+
/** A subscription to events for when the tab body begins centering. */
70+
private _centeringSub: Subscription;
71+
72+
constructor(
73+
_componentFactoryResolver: ComponentFactoryResolver,
74+
_viewContainerRef: ViewContainerRef,
75+
private _host: MdTabBody) {
76+
super(_componentFactoryResolver, _viewContainerRef);
77+
}
78+
79+
/** Set initial visibility or set up subscription for changing visibility. */
80+
ngOnInit(): void {
81+
if (this._host._isCenterPosition(this._host._position)) {
82+
this.attach(this._host._content);
83+
} else {
84+
this._centeringSub = this._host.onCentering.subscribe(() => {
85+
this.attach(this._host._content);
86+
this._centeringSub.unsubscribe();
87+
});
88+
}
89+
}
90+
91+
/** Clean up subscription if necessary. */
92+
ngOnDestroy(): void {
93+
if (this._centeringSub && !this._centeringSub.closed) {
94+
this._centeringSub.unsubscribe();
95+
}
96+
}
97+
}
98+
5599
/**
56100
* Wrapper for the contents of a tab.
57101
* @docs-private
@@ -88,9 +132,7 @@ export type MdTabBodyOriginState = 'left' | 'right';
88132
])
89133
]
90134
})
91-
export class MdTabBody implements OnInit, AfterViewChecked {
92-
/** The portal host inside of this container into which the tab body content will be loaded. */
93-
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
135+
export class MdTabBody implements OnInit {
94136

95137
/** Event emitted when the tab begins to animate towards the center as the active tab. */
96138
@Output() onCentering: EventEmitter<number> = new EventEmitter<number>();
@@ -128,6 +170,9 @@ export class MdTabBody implements OnInit, AfterViewChecked {
128170
}
129171
}
130172

173+
/** Whether the tab body should be visible. */
174+
visible = false;
175+
131176
constructor(private _elementRef: ElementRef,
132177
@Optional() private _dir: Directionality) { }
133178

@@ -138,29 +183,21 @@ export class MdTabBody implements OnInit, AfterViewChecked {
138183
ngOnInit() {
139184
if (this._position == 'center' && this._origin) {
140185
this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center';
186+
this.visible = true;
141187
}
142188
}
143189

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) {
190+
_onTranslateTabStarted(e: AnimationEvent): void {
155191
if (this._isCenterPosition(e.toState)) {
156192
this.onCentering.emit(this._elementRef.nativeElement.clientHeight);
193+
this.visible = true;
157194
}
158195
}
159196

160-
_onTranslateTabComplete(e: AnimationEvent) {
161-
// If the end state is that the tab is not centered, then detach the content.
197+
_onTranslateTabComplete(e: AnimationEvent): void {
198+
// If the end state is that the tab is not centered, emit an event.
162199
if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) {
163-
this._portalHost.detach();
200+
this.visible = false;
164201
}
165202

166203
// If the transition to the center is complete, emit an event.
@@ -175,7 +212,7 @@ export class MdTabBody implements OnInit, AfterViewChecked {
175212
}
176213

177214
/** Whether the provided position state is considered center, regardless of origin. */
178-
private _isCenterPosition(position: MdTabBodyPositionState|string): boolean {
215+
_isCenterPosition(position: MdTabBodyPositionState|string): boolean {
179216
return position == 'center' ||
180217
position == 'left-origin-center' ||
181218
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
@@ -30,15 +30,22 @@ describe('MdTabGroup', () => {
3030

3131
describe('basic behavior', () => {
3232
let fixture: ComponentFixture<SimpleTabsTestApp>;
33+
let element: HTMLElement;
3334

3435
beforeEach(() => {
3536
fixture = TestBed.createComponent(SimpleTabsTestApp);
37+
element = fixture.nativeElement;
3638
});
3739

3840
it('should default to the first tab', () => {
3941
checkSelectedIndex(1, fixture);
4042
});
4143

44+
it('will properly load content on first change detection pass', () => {
45+
fixture.detectChanges();
46+
expect(element.querySelectorAll('.mat-tab-body')[1].querySelectorAll('span').length).toBe(3);
47+
});
48+
4249
it('should change selected index on click', () => {
4350
let component = fixture.debugElement.componentInstance;
4451
component.selectedIndex = 0;
@@ -322,23 +329,26 @@ describe('MdTabGroup', () => {
322329
fixture.debugElement.query(By.directive(MdTabGroup)).componentInstance as MdTabGroup;
323330
});
324331

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

329336
tabGroup.selectedIndex = 2;
330337
fixture.detectChanges();
338+
// Use whenStable to wait for async observables and change detection to run in content.
339+
fixture.whenStable().then(() => {
331340

332-
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
333-
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
341+
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
342+
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
334343

335-
fixture.componentInstance.otherLabel = 'Chips';
336-
fixture.componentInstance.otherContent = 'Salt, vinegar';
337-
fixture.detectChanges();
344+
fixture.componentInstance.otherLabel = 'Chips';
345+
fixture.componentInstance.otherContent = 'Salt, vinegar';
346+
fixture.detectChanges();
338347

339-
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
340-
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
341-
});
348+
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
349+
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
350+
});
351+
}));
342352

343353
it('should support @ViewChild in the tab content', () => {
344354
expect(fixture.componentInstance.legumes).toBeTruthy();
@@ -419,7 +429,7 @@ describe('nested MdTabGroup with enabled animations', () => {
419429
</md-tab>
420430
<md-tab>
421431
<ng-template md-tab-label>Tab Two</ng-template>
422-
Tab two content
432+
<span>Tab </span><span>two</span><span>content</span>
423433
</md-tab>
424434
<md-tab>
425435
<ng-template md-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 {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)