From 469b1d1c2ae12fefa38bf14c740adb76c6b9408a Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 4 Sep 2017 11:46:31 -0500 Subject: [PATCH 1/7] feat(tabs): adds ability for lazy loaded tabs --- src/demo-app/demo-app/demo-module.ts | 5 ++++- src/demo-app/tabs/tabs-demo.html | 14 ++++++++++++++ src/demo-app/tabs/tabs-demo.ts | 14 ++++++++++++++ src/lib/tabs/index.ts | 5 ++++- src/lib/tabs/tab-body.ts | 3 ++- src/lib/tabs/tab-content.ts | 6 ++++++ src/lib/tabs/tab.html | 7 ++++++- src/lib/tabs/tab.ts | 4 +++- 8 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/lib/tabs/tab-content.ts diff --git a/src/demo-app/demo-app/demo-module.ts b/src/demo-app/demo-app/demo-module.ts index 79de0624a050..0471565f71f5 100644 --- a/src/demo-app/demo-app/demo-module.ts +++ b/src/demo-app/demo-app/demo-module.ts @@ -30,7 +30,9 @@ import {SidenavDemo} from '../sidenav/sidenav-demo'; import {SnackBarDemo} from '../snack-bar/snack-bar-demo'; import {PortalDemo, ScienceJoke} from '../portal/portal-demo'; import {MenuDemo} from '../menu/menu-demo'; -import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from '../tabs/tabs-demo'; +import { + FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo, Counter +} from '../tabs/tabs-demo'; import {PlatformDemo} from '../platform/platform-demo'; import {AutocompleteDemo} from '../autocomplete/autocomplete-demo'; import {InputDemo} from '../input/input-demo'; @@ -103,6 +105,7 @@ import {TableHeaderDemo} from '../table/table-header-demo'; SunnyTabContent, RainyTabContent, FoggyTabContent, + Counter, PlatformDemo, TypographyDemo, ExpansionDemo, diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index e600753a33e8..fd9a70d9fed7 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -277,3 +277,17 @@

Tabs with background color

+ +

Lazy Loaded Tabs

+ + + + + + + + + + + + diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index d5eedce2ef86..af40522c22c1 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -124,3 +124,17 @@ export class RainyTabContent {} template: 'This is the routed body of the foggy tab.', }) export class FoggyTabContent {} + + +@Component({ + moduleId: module.id, + selector: 'counter', + template: `{{count}}` +}) +export class Counter { + count = 0; + ngOnInit() { + this.count++; + console.log('Counting...', this.count); + } +} diff --git a/src/lib/tabs/index.ts b/src/lib/tabs/index.ts index fec6353800b1..663a87f27ffc 100644 --- a/src/lib/tabs/index.ts +++ b/src/lib/tabs/index.ts @@ -20,6 +20,7 @@ import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar'; import {MdInkBar} from './ink-bar'; import {MdTabBody} from './tab-body'; import {MdTabHeader} from './tab-header'; +import {MdTabContent} from './tab-content'; @NgModule({ @@ -39,6 +40,7 @@ import {MdTabHeader} from './tab-header'; MdTab, MdTabNav, MdTabLink, + MdTabContent, ], declarations: [ MdTabGroup, @@ -49,7 +51,8 @@ import {MdTabHeader} from './tab-header'; MdTabNav, MdTabLink, MdTabBody, - MdTabHeader + MdTabHeader, + MdTabContent, ], providers: [VIEWPORT_RULER_PROVIDER], }) diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index ce54ea95778c..8796214c8862 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -146,7 +146,8 @@ export class MdTabBody implements OnInit, AfterViewChecked { */ ngAfterViewChecked() { if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) { - this._portalHost.attach(this._content); + // Nested templates via mdTabContent templates causes expression change error + Promise.resolve().then(() => this._portalHost.attach(this._content)); } } diff --git a/src/lib/tabs/tab-content.ts b/src/lib/tabs/tab-content.ts new file mode 100644 index 000000000000..ed856dd21cd6 --- /dev/null +++ b/src/lib/tabs/tab-content.ts @@ -0,0 +1,6 @@ +import {Directive, TemplateRef} from '@angular/core'; + +@Directive({ selector: '[mdTabContent]' }) +export class MdTabContent { + constructor(public template: TemplateRef) { } +} diff --git a/src/lib/tabs/tab.html b/src/lib/tabs/tab.html index 398c819fa896..96ac5c123d48 100644 --- a/src/lib/tabs/tab.html +++ b/src/lib/tabs/tab.html @@ -1,4 +1,9 @@ - + + + + + diff --git a/src/lib/tabs/tab.ts b/src/lib/tabs/tab.ts index 37778064e09d..9fd2ee206a86 100644 --- a/src/lib/tabs/tab.ts +++ b/src/lib/tabs/tab.ts @@ -23,6 +23,7 @@ import { } from '@angular/core'; import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled'; import {MdTabLabel} from './tab-label'; +import {MdTabContent} from './tab-content'; import {Subject} from 'rxjs/Subject'; // Boilerplate for applying mixins to MdTab. @@ -42,9 +43,10 @@ export const _MdTabMixinBase = mixinDisabled(MdTabBase); export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChanges, OnDestroy { /** Content for the tab label given by . */ @ContentChild(MdTabLabel) templateLabel: MdTabLabel; + @ContentChild(MdTabContent) templateBody: MdTabContent; /** Template inside the MdTab view that contains an . */ - @ViewChild(TemplateRef) _content: TemplateRef; + @ViewChild('bodyTemplate') _content: TemplateRef; /** The plain text label for the tab, used when there is no template label. */ @Input('label') textLabel: string = ''; From 7ed6b51957470f42690f7066ff37740430f5e654 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 6 Sep 2017 18:26:31 -0500 Subject: [PATCH 2/7] chore(test): add test for lazy loaded --- src/lib/tabs/tab-group.spec.ts | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index 439367351a20..939cfb244d87 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -376,6 +376,34 @@ describe('nested MdTabGroup 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', (done) => { + 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(); + done(); + }); + }); +}); + + @Component({ template: ` + + Eager + + + +
Hi
+
+
+
+ `, +}) +class TemplateTabs {} From f66455aa8ffd4d6f6528a59ef473cea8356fcacd Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 6 Sep 2017 18:58:47 -0500 Subject: [PATCH 3/7] chore(nit): address PR feedback --- src/lib/tabs/tab-body.ts | 14 +++++++------- src/lib/tabs/tab-group.spec.ts | 29 ++++++++++++++++------------- src/lib/tabs/tab-group.ts | 6 +++--- src/lib/tabs/tab.html | 7 +------ src/lib/tabs/tab.ts | 19 +++++++++++-------- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index 8796214c8862..866a415d9734 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -15,7 +15,7 @@ import { OnInit, ElementRef, Optional, - AfterViewChecked, + DoCheck, ViewEncapsulation, ChangeDetectionStrategy, } from '@angular/core'; @@ -87,7 +87,7 @@ export type MdTabBodyOriginState = 'left' | 'right'; ]) ] }) -export class MdTabBody implements OnInit, AfterViewChecked { +export class MdTabBody implements OnInit, DoCheck { /** The portal host inside of this container into which the tab body content will be loaded. */ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; @@ -134,7 +134,7 @@ export class MdTabBody implements OnInit, AfterViewChecked { * 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'; } @@ -144,20 +144,20 @@ export class MdTabBody implements OnInit, AfterViewChecked { * After the view has been set, check if the tab content is set to the center and attach the * content if it is not already attached. */ - ngAfterViewChecked() { + ngDoCheck(): void { if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) { // Nested templates via mdTabContent templates causes expression change error - Promise.resolve().then(() => this._portalHost.attach(this._content)); + this._portalHost.attach(this._content); } } - _onTranslateTabStarted(e: AnimationEvent) { + _onTranslateTabStarted(e: AnimationEvent): void { if (this._isCenterPosition(e.toState)) { this.onCentering.emit(this._elementRef.nativeElement.clientHeight); } } - _onTranslateTabComplete(e: AnimationEvent) { + _onTranslateTabComplete(e: AnimationEvent): void { // If the end state is that the tab is not centered, then detach the content. if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) { this._portalHost.detach(); diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index 939cfb244d87..9888d97a6ca3 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -293,23 +293,26 @@ describe('MdTabGroup', () => { fixture.debugElement.query(By.directive(MdTabGroup)).componentInstance as MdTabGroup; }); - it('should support a tab-group with the simple api', () => { - expect(getSelectedLabel(fixture).textContent).toMatch('Junk food'); - expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries'); + it('should support a tab-group with the simple api', async(() => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(getSelectedLabel(fixture).textContent).toMatch('Junk food'); + expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries'); - tabGroup.selectedIndex = 2; - fixture.detectChanges(); + tabGroup.selectedIndex = 2; + fixture.detectChanges(); - expect(getSelectedLabel(fixture).textContent).toMatch('Fruit'); - expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes'); + expect(getSelectedLabel(fixture).textContent).toMatch('Fruit'); + expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes'); - fixture.componentInstance.otherLabel = 'Chips'; - fixture.componentInstance.otherContent = 'Salt, vinegar'; - fixture.detectChanges(); + fixture.componentInstance.otherLabel = 'Chips'; + fixture.componentInstance.otherContent = 'Salt, vinegar'; + fixture.detectChanges(); - expect(getSelectedLabel(fixture).textContent).toMatch('Chips'); - expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar'); - }); + expect(getSelectedLabel(fixture).textContent).toMatch('Chips'); + expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar'); + }); + })); it('should support @ViewChild in the tab content', () => { expect(fixture.componentInstance.legumes).toBeTruthy(); diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 8c3bf330cc40..0b43debf72a9 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -187,7 +187,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit } } - ngAfterContentInit() { + ngAfterContentInit(): void { this._subscribeToTabLabels(); // Subscribe to changes in the amount of tabs, in order to be @@ -198,7 +198,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit }); } - ngOnDestroy() { + ngOnDestroy(): void { this._tabsSubscription.unsubscribe(); this._tabLabelSubscription.unsubscribe(); } @@ -211,7 +211,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit this._isInitialized = true; } - _focusChanged(index: number) { + _focusChanged(index: number): void { this.focusChange.emit(this._createChangeEvent(index)); } diff --git a/src/lib/tabs/tab.html b/src/lib/tabs/tab.html index 96ac5c123d48..398c819fa896 100644 --- a/src/lib/tabs/tab.html +++ b/src/lib/tabs/tab.html @@ -1,9 +1,4 @@ - - - - - + diff --git a/src/lib/tabs/tab.ts b/src/lib/tabs/tab.ts index 9fd2ee206a86..40ffa472566f 100644 --- a/src/lib/tabs/tab.ts +++ b/src/lib/tabs/tab.ts @@ -43,10 +43,12 @@ export const _MdTabMixinBase = mixinDisabled(MdTabBase); export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChanges, OnDestroy { /** Content for the tab label given by . */ @ContentChild(MdTabLabel) templateLabel: MdTabLabel; - @ContentChild(MdTabContent) templateBody: MdTabContent; + + /** User provided template that we are going to use instead of implicitContent template */ + @ContentChild(MdTabContent, {read: TemplateRef}) _explicitContent: TemplateRef; /** Template inside the MdTab view that contains an . */ - @ViewChild('bodyTemplate') _content: TemplateRef; + @ViewChild(TemplateRef) _implicitContent: TemplateRef; /** The plain text label for the tab, used when there is no template label. */ @Input('label') textLabel: string = ''; @@ -79,17 +81,18 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChan super(); } - ngOnChanges(changes: SimpleChanges) { + ngOnInit(): void { + this._contentPortal = new TemplatePortal( + this._explicitContent || this._implicitContent, this._viewContainerRef); + } + + ngOnChanges(changes: SimpleChanges): void { if (changes.hasOwnProperty('textLabel')) { this._labelChange.next(); } } - ngOnDestroy() { + ngOnDestroy(): void { this._labelChange.complete(); } - - ngOnInit() { - this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef); - } } From db46d849c58dff01f39156b9d363c515c435c796 Mon Sep 17 00:00:00 2001 From: Austin Date: Sat, 9 Sep 2017 11:19:00 -0500 Subject: [PATCH 4/7] chore(nit): update per feedback --- src/lib/tabs/tab-body.ts | 4 +++- src/lib/tabs/tab-content.ts | 2 +- src/lib/tabs/tab-group.spec.ts | 3 +-- src/lib/tabs/tabs.md | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index 866a415d9734..fb745fb07a68 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -146,7 +146,9 @@ export class MdTabBody implements OnInit, DoCheck { */ ngDoCheck(): void { if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) { - // Nested templates via mdTabContent templates causes expression change error + // It is important to attach the view during `DoCheck`; if an + // embedded view is created during change detection, it will either + // cause a changed-after-checked error or never be checked at all. this._portalHost.attach(this._content); } } diff --git a/src/lib/tabs/tab-content.ts b/src/lib/tabs/tab-content.ts index ed856dd21cd6..3c64ad67f3b4 100644 --- a/src/lib/tabs/tab-content.ts +++ b/src/lib/tabs/tab-content.ts @@ -1,6 +1,6 @@ import {Directive, TemplateRef} from '@angular/core'; -@Directive({ selector: '[mdTabContent]' }) +@Directive({selector: '[mdTabContent]'}) export class MdTabContent { constructor(public template: TemplateRef) { } } diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index 9888d97a6ca3..5c59d0a4cad9 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -389,7 +389,7 @@ describe('lazy loaded tabs', () => { TestBed.compileComponents(); })); - it('should lazy load the second tab', (done) => { + it('should lazy load the second tab', async () => { let fixture = TestBed.createComponent(TemplateTabs); fixture.detectChanges(); @@ -401,7 +401,6 @@ describe('lazy loaded tabs', () => { fixture.detectChanges(); let child = fixture.debugElement.query(By.css('.child')); expect(child.nativeElement).toBeDefined(); - done(); }); }); }); diff --git a/src/lib/tabs/tabs.md b/src/lib/tabs/tabs.md index e24e7a1b0a30..d6e5526f264a 100644 --- a/src/lib/tabs/tabs.md +++ b/src/lib/tabs/tabs.md @@ -79,3 +79,27 @@ provides a tab-like UI for navigating between routes. The tab-nav-bar is not tied to any particular router; it works with normal `` elements and uses the `active` property to determine which tab is currently active. The corresponding `` 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 + + + + The First Content + + + + + The Second Content + + + +``` From 17ef473f16b0c17e212e1566573533d2aec7aa9b Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 17 Sep 2017 11:55:07 -0500 Subject: [PATCH 5/7] chore(merge): fix merge --- src/lib/tabs/index.ts | 3 +-- src/lib/tabs/tab-content.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/tabs/index.ts b/src/lib/tabs/index.ts index b2795d0d8928..f93e7c31d564 100644 --- a/src/lib/tabs/index.ts +++ b/src/lib/tabs/index.ts @@ -6,5 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ - export * from './public_api'; - \ No newline at end of file +export * from './public_api'; diff --git a/src/lib/tabs/tab-content.ts b/src/lib/tabs/tab-content.ts index 3c64ad67f3b4..2dd3e23d3157 100644 --- a/src/lib/tabs/tab-content.ts +++ b/src/lib/tabs/tab-content.ts @@ -1,4 +1,12 @@ -import {Directive, TemplateRef} from '@angular/core'; +/** + * @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 { From aa2df3ac7e663f1e0d302cb28496e81737ec3b42 Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 21 Sep 2017 16:38:27 -0500 Subject: [PATCH 6/7] chore(merge): fix bad merge --- src/lib/tabs/tabs-module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/tabs/tabs-module.ts b/src/lib/tabs/tabs-module.ts index ee93f03ec4f4..fee31780a6d3 100644 --- a/src/lib/tabs/tabs-module.ts +++ b/src/lib/tabs/tabs-module.ts @@ -20,6 +20,7 @@ import {MdTabHeader} from './tab-header'; import {MdTabLabel} from './tab-label'; import {MdTabLabelWrapper} from './tab-label-wrapper'; import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar'; +import {MdTabContent} from './tab-content'; @NgModule({ @@ -39,6 +40,7 @@ import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar'; MdTab, MdTabNav, MdTabLink, + MdTabContent, ], declarations: [ MdTabGroup, @@ -49,7 +51,8 @@ import {MdTabLink, MdTabNav} from './tab-nav-bar/tab-nav-bar'; MdTabNav, MdTabLink, MdTabBody, - MdTabHeader + MdTabHeader, + MdTabContent, ], providers: [VIEWPORT_RULER_PROVIDER], }) From 4741f77c708fc67e236a690f6fb4d1cd6f39a9d4 Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 10 Dec 2017 13:10:05 -0600 Subject: [PATCH 7/7] fix(tabs): add export for new directive --- src/lib/tabs/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/tabs/public-api.ts b/src/lib/tabs/public-api.ts index 5bdae5816899..c4cc051443ed 100644 --- a/src/lib/tabs/public-api.ts +++ b/src/lib/tabs/public-api.ts @@ -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';