Skip to content

feat(tabs): adds ability for lazy loaded tabs #6832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
5 changes: 4 additions & 1 deletion src/demo-app/demo-app/demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ import {TableDemo} from '../table/table-demo';
import {ScreenTypeDemo} from '../screen-type/screen-type-demo';
import {LayoutModule} from '@angular/cdk/layout';
import {TableHeaderDemo} from '../table/table-header-demo';
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from '../tabs/tabs-demo';
import {
FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo, Counter
} from '../tabs/tabs-demo';
import {ToolbarDemo} from '../toolbar/toolbar-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
import {TypographyDemo} from '../typography/typography-demo';
Expand Down Expand Up @@ -123,6 +125,7 @@ import {DEMO_APP_ROUTES} from './routes';
ToolbarDemo,
TooltipDemo,
TypographyDemo,
Counter,
],
providers: [
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},
Expand Down
18 changes: 16 additions & 2 deletions src/demo-app/tabs/tabs-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,22 @@ <h1>Tabs with background color</h1>
<div class="tab-content">
This tab is about combustion!
</div>
</mat-tab>
</mat-tab-group>
</md-tab>
</md-tab-group>

<h1>Lazy Loaded Tabs</h1>
<md-tab-group>
<md-tab label="First">
<ng-template mdTabContent>
<counter></counter>
</ng-template>
</md-tab>
<md-tab label="Second">
<ng-template mdTabContent>
<counter></counter>
</ng-template>
</md-tab>
</md-tab-group>

<h1>Tabs with autosize textarea</h1>
<mat-tab-group class="demo-tab-group">
Expand Down
14 changes: 14 additions & 0 deletions src/demo-app/tabs/tabs-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,17 @@ export class RainyTabContent {}
template: 'This is the routed body of the foggy tab.',
})
export class FoggyTabContent {}


@Component({
moduleId: module.id,
selector: 'counter',
template: `<span>{{count}}</span>`
})
export class Counter {
count = 0;
ngOnInit() {
this.count++;
console.log('Counting...', this.count);
}
}
1 change: 1 addition & 0 deletions src/lib/tabs/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export {MatTabLabelWrapper} from './tab-label-wrapper';
export {MatTab} from './tab';
export {MatTabLabel} from './tab-label';
export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
export {MdTabContent} from './tab-content';
7 changes: 5 additions & 2 deletions src/lib/tabs/tab-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
])
]
})
export class MatTabBody implements OnInit {
export class MdTabBody implements OnInit {
/** The portal host inside of this container into which the tab body content will be loaded. */
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;

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

Expand Down Expand Up @@ -190,7 +193,7 @@ export class MatTabBody implements OnInit {
* After initialized, check if the content is centered and has an origin. If so, set the
* special position states that transition the tab from the left or right before centering.
*/
ngOnInit() {
ngOnInit(): void {
if (this._position == 'center' && this._origin) {
this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center';
}
Expand Down
14 changes: 14 additions & 0 deletions src/lib/tabs/tab-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, TemplateRef} from '@angular/core';

@Directive({selector: '[mdTabContent]'})
export class MdTabContent {
constructor(public template: TemplateRef<any>) { }
}
44 changes: 44 additions & 0 deletions src/lib/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,33 @@ describe('nested MatTabGroup with enabled animations', () => {
});


describe('lazy loaded tabs', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdTabsModule, BrowserAnimationsModule],
declarations: [TemplateTabs]
});

TestBed.compileComponents();
}));

it('should lazy load the second tab', async () => {
let fixture = TestBed.createComponent(TemplateTabs);
fixture.detectChanges();

let tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];
tabLabel.nativeElement.click();
fixture.detectChanges();

fixture.whenStable().then(() => {
fixture.detectChanges();
let child = fixture.debugElement.query(By.css('.child'));
expect(child.nativeElement).toBeDefined();
});
});
});


@Component({
template: `
<mat-tab-group class="tab-group"
Expand Down Expand Up @@ -602,3 +629,20 @@ class TabGroupWithSimpleApi {
})
class NestedTabs {}


@Component({
selector: 'template-tabs',
template: `
<md-tab-group>
<md-tab label="One">
Eager
</md-tab>
<md-tab label="Two">
<ng-template mdTabContent>
<div class="child">Hi</div>
</ng-template>
</md-tab>
</md-tab-group>
`,
})
class TemplateTabs {}
4 changes: 2 additions & 2 deletions src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
}
}

ngAfterContentInit() {
ngAfterContentInit(): void {
this._subscribeToTabLabels();

// Subscribe to changes in the amount of tabs, in order to be
Expand All @@ -212,7 +212,7 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
});
}

ngOnDestroy() {
ngOnDestroy(): void {
this._tabsSubscription.unsubscribe();
this._tabLabelSubscription.unsubscribe();
}
Expand Down
16 changes: 11 additions & 5 deletions src/lib/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import {CanDisable, mixinDisabled} from '@angular/material/core';
import {Subject} from 'rxjs/Subject';
import {MatTabLabel} from './tab-label';
import {MdTabContent} from './tab-content';


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

/** Template inside the MatTab view that contains an <ng-content>. */
@ViewChild(TemplateRef) _content: TemplateRef<any>;
/** User provided template that we are going to use instead of implicitContent template */
@ContentChild(MdTabContent, {read: TemplateRef}) _explicitContent: TemplateRef<any>;

/** Template inside the MdTab view that contains an <ng-content>. */
@ViewChild(TemplateRef) _implicitContent: TemplateRef<any>;

/** The plain text label for the tab, used when there is no template label. */
@Input('label') textLabel: string = '';
Expand Down Expand Up @@ -86,6 +90,11 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh
super();
}

ngOnInit(): void {
this._contentPortal = new TemplatePortal(
this._explicitContent || this._implicitContent, this._viewContainerRef);
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty('textLabel')) {
this._labelChange.next();
Expand All @@ -101,7 +110,4 @@ export class MatTab extends _MatTabMixinBase implements OnInit, CanDisable, OnCh
this._labelChange.complete();
}

ngOnInit(): void {
this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
}
}
6 changes: 4 additions & 2 deletions src/lib/tabs/tabs-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {MatTabHeader} from './tab-header';
import {MatTabLabel} from './tab-label';
import {MatTabLabelWrapper} from './tab-label-wrapper';
import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';

import {MdTabContent} from './tab-content';

@NgModule({
imports: [
Expand All @@ -39,6 +39,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
MatTab,
MatTabNav,
MatTabLink,
MdTabContent,
],
declarations: [
MatTabGroup,
Expand All @@ -50,7 +51,8 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
MatTabLink,
MatTabBody,
MatTabBodyPortal,
MatTabHeader
MatTabHeader,
MdTabContent,
],
providers: [VIEWPORT_RULER_PROVIDER],
})
Expand Down
28 changes: 24 additions & 4 deletions src/lib/tabs/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ tab labels in the header.
<!-- example(tabs-overview) -->

### Events

The `selectedTabChange` output event is emitted when the active tab changes.

The `focusChange` output event is emitted when the user puts focus on any of the tab labels in
the header, usually through keyboard navigation.

### Labels

If a tab's label is only text then the simple tab-group API can be used.

```html
Expand Down Expand Up @@ -54,7 +52,6 @@ For more complex labels, add a template with the `mat-tab-label` directive insid
```

### Dynamic Height

By default, the tab group will not change its height to the height of the currently active tab. To
change this, set the `dynamicHeight` input to true. The tab body will animate its height according
to the height of the active tab.
Expand All @@ -80,11 +77,34 @@ The `tab-nav-bar` is not tied to any particular router; it works with normal `<a
the `active` property to determine which tab is currently active. The corresponding
`<router-outlet>` can be placed anywhere in the view.

## Lazy Loading
By default, the tab contents are eagerly loaded. Eagerly loaded tabs
will initalize the child components but not inject them into the DOM
until the tab is activated.

If the tab contains several complex child components, it is advised
to lazy load the tab's content. Tab contents can be lazy loaded by
declaring the body in a `ng-template` with the `mdTabContent` attribute.

```html
<md-tab-group>
<md-tab label="First">
<ng-template mdTabContent>
The First Content
</ng-template>
</md-tab>
<md-tab label="Second">
<ng-template mdTabContent>
The Second Content
</ng-template>
</md-tab>
</md-tab-group>
```

### Accessibility
Tabs without text or labels should be given a meaningful label via `aria-label` or
`aria-labelledby`. For `MatTabNav`, the `<nav>` element should have a label as well.


#### Keyboard shortcuts

| Shortcut | Action |
Expand Down