Skip to content

Commit 51d7742

Browse files
amcdnljosephperrott
authored andcommitted
feat(tabs): adds ability for lazy loaded tabs
1 parent d914cc4 commit 51d7742

File tree

10 files changed

+143
-17
lines changed

10 files changed

+143
-17
lines changed

src/demo-app/demo-app/demo-module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import {SidenavDemo} from '../sidenav/sidenav-demo';
3030
import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
3131
import {PortalDemo, ScienceJoke} from '../portal/portal-demo';
3232
import {MenuDemo} from '../menu/menu-demo';
33-
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from '../tabs/tabs-demo';
33+
import {
34+
FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo, Counter
35+
} from '../tabs/tabs-demo';
3436
import {PlatformDemo} from '../platform/platform-demo';
3537
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
3638
import {InputDemo} from '../input/input-demo';
@@ -103,6 +105,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
103105
SunnyTabContent,
104106
RainyTabContent,
105107
FoggyTabContent,
108+
Counter,
106109
PlatformDemo,
107110
TypographyDemo,
108111
ExpansionDemo,

src/demo-app/tabs/tabs-demo.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,17 @@ <h1>Tabs with background color</h1>
277277
</div>
278278
</md-tab>
279279
</md-tab-group>
280+
281+
<h1>Lazy Loaded Tabs</h1>
282+
<md-tab-group>
283+
<md-tab label="First">
284+
<ng-template mdTabContent>
285+
<counter></counter>
286+
</ng-template>
287+
</md-tab>
288+
<md-tab label="Second">
289+
<ng-template mdTabContent>
290+
<counter></counter>
291+
</ng-template>
292+
</md-tab>
293+
</md-tab-group>

src/demo-app/tabs/tabs-demo.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,17 @@ export class RainyTabContent {}
125125
template: 'This is the routed body of the foggy tab.',
126126
})
127127
export class FoggyTabContent {}
128+
129+
130+
@Component({
131+
moduleId: module.id,
132+
selector: 'counter',
133+
template: `<span>{{count}}</span>`
134+
})
135+
export class Counter {
136+
count = 0;
137+
ngOnInit() {
138+
this.count++;
139+
console.log('Counting...', this.count);
140+
}
141+
}

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 cdkPortalHost></ng-template>
5+
<ng-template tabBodyHost></ng-template>
66
</div>

src/lib/tabs/tab-body.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import {
1111
Component,
1212
Input,
1313
Output,
14+
Directive,
1415
EventEmitter,
1516
OnInit,
1617
ElementRef,
1718
Optional,
18-
AfterViewChecked,
19+
DoCheck,
1920
ViewEncapsulation,
2021
ChangeDetectionStrategy,
22+
ComponentFactoryResolver,
23+
ViewContainerRef,
2124
} from '@angular/core';
2225
import {
2326
trigger,
@@ -31,6 +34,30 @@ import {TemplatePortal, PortalHostDirective} from '@angular/cdk/portal';
3134
import {Directionality, Direction} from '@angular/cdk/bidi';
3235

3336

37+
38+
39+
40+
41+
@Directive({
42+
selector: '[tabBodyHost]'
43+
})
44+
export class TabsBodyPortal extends PortalHostDirective {
45+
constructor(
46+
_componentFactoryResolver: ComponentFactoryResolver,
47+
_viewContainerRef: ViewContainerRef,
48+
private host: MdTabBody) {
49+
super(_componentFactoryResolver, _viewContainerRef);
50+
}
51+
52+
ngOnInit() {
53+
this.host._setVisibleState();
54+
this.host.onCentered.subscribe(() => this.host._setVisibleState());
55+
}
56+
}
57+
58+
59+
60+
3461
/**
3562
* These position states are used internally as animation states for the tab body. Setting the
3663
* position state to left, right, or center will transition the tab body from its current
@@ -88,9 +115,9 @@ export type MdTabBodyOriginState = 'left' | 'right';
88115
])
89116
]
90117
})
91-
export class MdTabBody implements OnInit, AfterViewChecked {
118+
export class MdTabBody implements OnInit {
92119
/** The portal host inside of this container into which the tab body content will be loaded. */
93-
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
120+
@ViewChild(TabsBodyPortal) _portalHost: TabsBodyPortal;
94121

95122
/** Event emitted when the tab begins to animate towards the center as the active tab. */
96123
@Output() onCentering: EventEmitter<number> = new EventEmitter<number>();
@@ -135,29 +162,29 @@ export class MdTabBody implements OnInit, AfterViewChecked {
135162
* After initialized, check if the content is centered and has an origin. If so, set the
136163
* special position states that transition the tab from the left or right before centering.
137164
*/
138-
ngOnInit() {
165+
ngOnInit(): void {
139166
if (this._position == 'center' && this._origin) {
140167
this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center';
141168
}
142169
}
143170

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() {
171+
/** Sets whether the tab body is currently visible. */
172+
_setVisibleState() {
149173
if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) {
174+
// It is important to attach the view during `DoCheck`; if an
175+
// embedded view is created during change detection, it will either
176+
// cause a changed-after-checked error or never be checked at all.
150177
this._portalHost.attach(this._content);
151178
}
152179
}
153180

154-
_onTranslateTabStarted(e: AnimationEvent) {
181+
_onTranslateTabStarted(e: AnimationEvent): void {
155182
if (this._isCenterPosition(e.toState)) {
156183
this.onCentering.emit(this._elementRef.nativeElement.clientHeight);
157184
}
158185
}
159186

160-
_onTranslateTabComplete(e: AnimationEvent) {
187+
_onTranslateTabComplete(e: AnimationEvent): void {
161188
// If the end state is that the tab is not centered, then detach the content.
162189
if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) {
163190
this._portalHost.detach();

src/lib/tabs/tab-content.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {Directive, TemplateRef} from '@angular/core';
2+
3+
@Directive({ selector: '[mdTabContent]' })
4+
export class MdTabContent {
5+
constructor(public template: TemplateRef<any>) { }
6+
}

src/lib/tabs/tab.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
<!-- Create a template for the content of the <mat-tab> so that we can grab a reference to this
22
TemplateRef and use it in a Portal to render the tab content in the appropriate place in the
33
tab-group. -->
4-
<ng-template><ng-content></ng-content></ng-template>
4+
<ng-template #bodyTemplate>
5+
<ng-content></ng-content>
6+
<ng-template *ngIf="templateBody && templateBody.template"
7+
[ngTemplateOutlet]="templateBody.template">
8+
</ng-template>
9+
</ng-template>

src/lib/tabs/tab.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ViewEncapsulation,
2323
} from '@angular/core';
2424
import {CanDisable, mixinDisabled} from '@angular/material/core';
25+
import {MdTabContent} from './tab-content';
2526
import {Subject} from 'rxjs/Subject';
2627
import {MdTabLabel} from './tab-label';
2728

@@ -44,9 +45,10 @@ export const _MdTabMixinBase = mixinDisabled(MdTabBase);
4445
export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChanges, OnDestroy {
4546
/** Content for the tab label given by <ng-template md-tab-label>. */
4647
@ContentChild(MdTabLabel) templateLabel: MdTabLabel;
48+
@ContentChild(MdTabContent) templateBody: MdTabContent;
4749

4850
/** Template inside the MdTab view that contains an <ng-content>. */
49-
@ViewChild(TemplateRef) _content: TemplateRef<any>;
51+
@ViewChild('bodyTemplate') _content: TemplateRef<any>;
5052

5153
/** The plain text label for the tab, used when there is no template label. */
5254
@Input('label') textLabel: string = '';

src/lib/tabs/tabs-module.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ 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, TabsBodyPortal} from './tab-body';
1818
import {MdTabGroup} from './tab-group';
1919
import {MdTabHeader} from './tab-header';
2020
import {MdTabLabel} from './tab-label';
2121
import {MdTabLabelWrapper} from './tab-label-wrapper';
2222
import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar';
23+
import {MdTabContent} from './tab-content';
2324

2425

2526
@NgModule({
@@ -39,17 +40,20 @@ import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar';
3940
MdTab,
4041
MdTabNav,
4142
MdTabLink,
43+
MdTabContent,
4244
],
4345
declarations: [
4446
MdTabGroup,
47+
TabsBodyPortal,
4548
MdTabLabel,
4649
MdTab,
4750
MdInkBar,
4851
MdTabLabelWrapper,
4952
MdTabNav,
5053
MdTabLink,
5154
MdTabBody,
52-
MdTabHeader
55+
MdTabHeader,
56+
MdTabContent,
5357
],
5458
providers: [VIEWPORT_RULER_PROVIDER],
5559
})

src/lib/tabs/tabs.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {Component} from '@angular/core';
2+
import {CommonModule} from '@angular/common';
3+
import {async, ComponentFixture, TestBed, flushMicrotasks, fakeAsync} from '@angular/core/testing';
4+
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
5+
import {MdTabsModule} from './public_api';
6+
7+
8+
fdescribe('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 pass after click.', () => {
28+
const firstTab = element.querySelectorAll('.mat-tab-label')[0] as HTMLElement;
29+
firstTab.click();
30+
fixture.detectChanges();
31+
32+
expect(element.querySelectorAll('.mat-tab-body')[0].querySelectorAll('.items').length).toBe(1);
33+
});
34+
it('won\'t pass without click.', () => {
35+
expect(element.querySelectorAll('.mat-tab-body')[0].querySelectorAll('.items').length).toBe(1);
36+
});
37+
});
38+
39+
@Component({
40+
template: `
41+
<md-tab-group>
42+
<md-tab label="tab 1">
43+
<p class="items"> content for tab 1</p>
44+
</md-tab>
45+
<md-tab label="tab 2">
46+
<p class="items"> content for tab 2</p>
47+
</md-tab>
48+
</md-tab-group>
49+
`
50+
})
51+
class TestTabs {}

0 commit comments

Comments
 (0)