Skip to content

Commit 6feaf62

Browse files
amcdnlmmalerba
authored andcommitted
feat(tabs): add ability to lazy load tab content (#8921)
* feat(tabs): add ability to lazy load tab content * fix: lint * chore: nit * chore: pr feedback * fix: presubmit error
1 parent 6a8ce02 commit 6feaf62

File tree

10 files changed

+137
-7
lines changed

10 files changed

+137
-7
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
5252
import {StepperDemo} from '../stepper/stepper-demo';
5353
import {ScreenTypeDemo} from '../screen-type/screen-type-demo';
5454
import {LayoutModule} from '@angular/cdk/layout';
55-
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from '../tabs/tabs-demo';
55+
import {
56+
FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo, Counter
57+
} from '../tabs/tabs-demo';
5658
import {ToolbarDemo} from '../toolbar/toolbar-demo';
5759
import {TooltipDemo} from '../tooltip/tooltip-demo';
5860
import {TypographyDemo} from '../typography/typography-demo';
@@ -87,6 +89,7 @@ import {TableDemoModule} from '../table/table-demo-module';
8789
ExpansionDemo,
8890
FocusOriginDemo,
8991
FoggyTabContent,
92+
Counter,
9093
GesturesDemo,
9194
GridListDemo,
9295
Home,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,20 @@ <h1>Tabs with autosize textarea</h1>
293293
</div>
294294
</mat-tab>
295295
</mat-tab-group>
296+
297+
<h1>Lazy Loaded Tabs</h1>
298+
<mat-tab-group>
299+
<mat-tab label="First">
300+
<ng-template matTabContent>
301+
<counter></counter>
302+
</ng-template>
303+
</mat-tab>
304+
<mat-tab label="Second">
305+
<ng-template matTabContent>
306+
<counter></counter>
307+
</ng-template>
308+
</mat-tab>
309+
<mat-tab label="Third">
310+
<counter></counter>
311+
</mat-tab>
312+
</mat-tab-group>

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,14 @@ export class RainyTabContent {}
133133
template: 'This is the routed body of the foggy tab.',
134134
})
135135
export class FoggyTabContent {}
136+
137+
@Component({
138+
moduleId: module.id,
139+
selector: 'counter',
140+
template: `<span>Content</span>`
141+
})
142+
export class Counter {
143+
ngOnInit() {
144+
console.log('Tab Loaded');
145+
}
146+
}

src/lib/tabs/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export {MatTabLabelWrapper} from './tab-label-wrapper';
2020
export {MatTab} from './tab';
2121
export {MatTabLabel} from './tab-label';
2222
export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
23+
export {MatTabContent} from './tab-content';
2324
export * from './tabs-animations';

src/lib/tabs/tab-body.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ import {
2222
ComponentFactoryResolver,
2323
ViewContainerRef,
2424
forwardRef,
25+
ViewChild,
2526
} from '@angular/core';
26-
import {AnimationEvent} from '@angular/animations';
27-
import {TemplatePortal, CdkPortalOutlet} from '@angular/cdk/portal';
27+
import {
28+
trigger,
29+
state,
30+
style,
31+
animate,
32+
transition,
33+
AnimationEvent,
34+
} from '@angular/animations';
35+
import {TemplatePortal, CdkPortalOutlet, PortalHostDirective} from '@angular/cdk/portal';
2836
import {Directionality, Direction} from '@angular/cdk/bidi';
2937
import {Subscription} from 'rxjs/Subscription';
3038
import {matTabsAnimations} from './tabs-animations';
@@ -130,6 +138,9 @@ export class MatTabBody implements OnInit {
130138
/** Event emitted when the tab completes its animation towards the center. */
131139
@Output() readonly _onCentered: EventEmitter<void> = new EventEmitter<void>(true);
132140

141+
/** The portal host inside of this container into which the tab body content will be loaded. */
142+
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
143+
133144
/** The tab body content to display. */
134145
@Input('content') _content: TemplatePortal;
135146

src/lib/tabs/tab-content.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, TemplateRef} from '@angular/core';
10+
11+
/** Decorates the `ng-template` tags and reads out the template from it. */
12+
@Directive({selector: '[matTabContent]'})
13+
export class MatTabContent {
14+
constructor(public template: TemplateRef<any>) { }
15+
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('MatTabGroup', () => {
1818
AsyncTabsTestApp,
1919
DisabledTabsTestApp,
2020
TabGroupWithSimpleApi,
21+
TemplateTabs,
2122
],
2223
});
2324

@@ -387,6 +388,23 @@ describe('MatTabGroup', () => {
387388
});
388389
});
389390

391+
describe('lazy loaded tabs', () => {
392+
it('should lazy load the second tab', async () => {
393+
let fixture = TestBed.createComponent(TemplateTabs);
394+
fixture.detectChanges();
395+
396+
let secondLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];
397+
secondLabel.nativeElement.click();
398+
fixture.detectChanges();
399+
400+
fixture.whenStable().then(() => {
401+
fixture.detectChanges();
402+
let child = fixture.debugElement.query(By.css('.child'));
403+
expect(child.nativeElement).toBeDefined();
404+
});
405+
});
406+
});
407+
390408
/**
391409
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
392410
* respective `active` classes
@@ -616,3 +634,19 @@ class TabGroupWithSimpleApi {
616634
})
617635
class NestedTabs {}
618636

637+
@Component({
638+
selector: 'template-tabs',
639+
template: `
640+
<mat-tab-group>
641+
<mat-tab label="One">
642+
Eager
643+
</mat-tab>
644+
<mat-tab label="Two">
645+
<ng-template matTabContent>
646+
<div class="child">Hi</div>
647+
</ng-template>
648+
</mat-tab>
649+
</mat-tab-group>
650+
`,
651+
})
652+
class TemplateTabs {}

src/lib/tabs/tab.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import {CanDisable, mixinDisabled} from '@angular/material/core';
2525
import {Subject} from 'rxjs/Subject';
2626
import {MatTabLabel} from './tab-label';
27+
import {MatTabContent} from './tab-content';
2728

2829

2930
// Boilerplate for applying mixins to MatTab.
@@ -45,8 +46,13 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh
4546
/** Content for the tab label given by `<ng-template mat-tab-label>`. */
4647
@ContentChild(MatTabLabel) templateLabel: MatTabLabel;
4748

48-
/** Template inside the MatTab view that contains an `<ng-content>`. */
49-
@ViewChild(TemplateRef) _content: TemplateRef<any>;
49+
/**
50+
* Template provided in the tab content that will be used if present, used to enable lazy-loading
51+
*/
52+
@ContentChild(MatTabContent, {read: TemplateRef}) _explicitContent: TemplateRef<any>;
53+
54+
/** Template inside the MatTab view that contains an <ng-content>. */
55+
@ViewChild(TemplateRef) _implicitContent: TemplateRef<any>;
5056

5157
/** The plain text label for the tab, used when there is no template label. */
5258
@Input('label') textLabel: string = '';
@@ -102,6 +108,7 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh
102108
}
103109

104110
ngOnInit(): void {
105-
this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
111+
this._contentPortal = new TemplatePortal(
112+
this._explicitContent || this._implicitContent, this._viewContainerRef);
106113
}
107114
}

src/lib/tabs/tabs-module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {MatTabHeader} from './tab-header';
2020
import {MatTabLabel} from './tab-label';
2121
import {MatTabLabelWrapper} from './tab-label-wrapper';
2222
import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
23+
import {MatTabContent} from './tab-content';
2324

2425

2526
@NgModule({
@@ -39,6 +40,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
3940
MatTab,
4041
MatTabNav,
4142
MatTabLink,
43+
MatTabContent,
4244
],
4345
declarations: [
4446
MatTabGroup,
@@ -50,7 +52,8 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
5052
MatTabLink,
5153
MatTabBody,
5254
MatTabBodyPortal,
53-
MatTabHeader
55+
MatTabHeader,
56+
MatTabContent,
5457
],
5558
providers: [VIEWPORT_RULER_PROVIDER],
5659
})

src/lib/tabs/tabs.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ The `tab-nav-bar` is not tied to any particular router; it works with normal `<a
8080
the `active` property to determine which tab is currently active. The corresponding
8181
`<router-outlet>` can be placed anywhere in the view.
8282

83+
## Lazy Loading
84+
By default, the tab contents are eagerly loaded. Eagerly loaded tabs
85+
will initalize the child components but not inject them into the DOM
86+
until the tab is activated.
87+
88+
89+
If the tab contains several complex child components or the tab's contents
90+
rely on DOM calculations during initialization, it is advised
91+
to lazy load the tab's content.
92+
93+
Tab contents can be lazy loaded by declaring the body in a `ng-template`
94+
with the `matTabContent` attribute.
95+
96+
```html
97+
<mat-tab-group>
98+
<mat-tab label="First">
99+
<ng-template matTabContent>
100+
The First Content
101+
</ng-template>
102+
</mat-tab>
103+
<mat-tab label="Second">
104+
<ng-template matTabContent>
105+
The Second Content
106+
</ng-template>
107+
</mat-tab>
108+
</mat-tab-group>
109+
```
110+
83111
### Accessibility
84112
Tabs without text or labels should be given a meaningful label via `aria-label` or
85113
`aria-labelledby`. For `MatTabNav`, the `<nav>` element should have a label as well.

0 commit comments

Comments
 (0)