Skip to content

Commit 7b1b614

Browse files
committed
chore(tabs): switch to OnPush change detection
Switches all of the tab-related components to OnPush change detection and sorts out the issues that come from the changes. Relates to #5035.
1 parent 077ebf6 commit 7b1b614

File tree

5 files changed

+99
-19
lines changed

5 files changed

+99
-19
lines changed

src/lib/tabs/tab-body.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Optional,
1818
AfterViewChecked,
1919
ViewEncapsulation,
20+
ChangeDetectionStrategy,
2021
} from '@angular/core';
2122
import {
2223
trigger,
@@ -59,6 +60,7 @@ export type MdTabBodyOriginState = 'left' | 'right';
5960
templateUrl: 'tab-body.html',
6061
styleUrls: ['tab-body.css'],
6162
encapsulation: ViewEncapsulation.None,
63+
changeDetection: ChangeDetectionStrategy.OnPush,
6264
host: {
6365
'class': 'mat-tab-body',
6466
},

src/lib/tabs/tab-group.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ import {
1616
ContentChildren,
1717
ElementRef,
1818
Renderer2,
19+
ChangeDetectionStrategy,
20+
ChangeDetectorRef,
21+
AfterViewChecked,
22+
AfterContentInit,
23+
AfterContentChecked,
24+
OnDestroy,
1925
} from '@angular/core';
2026
import {coerceBooleanProperty} from '../core';
2127
import {Observable} from 'rxjs/Observable';
28+
import {Subscription} from 'rxjs/Subscription';
2229
import {MdTab} from './tab';
2330
import {map} from '../core/rxjs/index';
31+
import {merge} from 'rxjs/observable/merge';
2432

2533

2634
/** Used to generate unique ID's for each tab component */
@@ -45,13 +53,16 @@ export type MdTabHeaderPosition = 'above' | 'below';
4553
selector: 'md-tab-group, mat-tab-group',
4654
templateUrl: 'tab-group.html',
4755
styleUrls: ['tab-group.css'],
56+
changeDetection: ChangeDetectionStrategy.OnPush,
4857
host: {
4958
'class': 'mat-tab-group',
5059
'[class.mat-tab-group-dynamic-height]': 'dynamicHeight',
5160
'[class.mat-tab-group-inverted-header]': 'headerPosition === "below"',
5261
}
5362
})
54-
export class MdTabGroup {
63+
export class MdTabGroup implements AfterContentInit, AfterContentChecked,
64+
AfterViewChecked, OnDestroy {
65+
5566
@ContentChildren(MdTab) _tabs: QueryList<MdTab>;
5667

5768
@ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef;
@@ -65,6 +76,12 @@ export class MdTabGroup {
6576
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
6677
private _tabBodyWrapperHeight: number = 0;
6778

79+
/** Subscription to tabs being added/removed. */
80+
private _tabsSubscription: Subscription;
81+
82+
/** Subscription to changes in the tab labels. */
83+
private _tabLabelSubscription: Subscription;
84+
6885
/** Whether the tab group should grow to the size of the active tab. */
6986
@Input()
7087
get dynamicHeight(): boolean { return this._dynamicHeight; }
@@ -82,17 +99,14 @@ export class MdTabGroup {
8299
set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); }
83100
private _disableRipple: boolean = false;
84101

85-
86-
private _selectedIndex: number | null = null;
87-
88102
/** The index of the active tab. */
89103
@Input()
90104
set selectedIndex(value: number | null) { this._indexToSelect = value; }
91105
get selectedIndex(): number | null { return this._selectedIndex; }
106+
private _selectedIndex: number | null = null;
92107

93108
/** Position of the tab header. */
94-
@Input()
95-
headerPosition: MdTabHeaderPosition = 'above';
109+
@Input() headerPosition: MdTabHeaderPosition = 'above';
96110

97111
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
98112
@Output() get selectedIndexChange(): Observable<number> {
@@ -107,7 +121,7 @@ export class MdTabGroup {
107121

108122
private _groupId: number;
109123

110-
constructor(private _renderer: Renderer2) {
124+
constructor(private _renderer: Renderer2, private _changeDetectorRef: ChangeDetectorRef) {
111125
this._groupId = nextId++;
112126
}
113127

@@ -141,7 +155,28 @@ export class MdTabGroup {
141155
}
142156
});
143157

144-
this._selectedIndex = indexToSelect;
158+
if (this._selectedIndex !== indexToSelect) {
159+
this._selectedIndex = indexToSelect;
160+
this._changeDetectorRef.markForCheck();
161+
}
162+
}
163+
164+
ngAfterContentInit() {
165+
this._subscribeToTabLabels();
166+
this._tabsSubscription = this._tabs.changes.subscribe(() => {
167+
this._subscribeToTabLabels();
168+
this._changeDetectorRef.markForCheck();
169+
});
170+
}
171+
172+
ngOnDestroy() {
173+
if (this._tabsSubscription) {
174+
this._tabsSubscription.unsubscribe();
175+
}
176+
177+
if (this._tabLabelSubscription) {
178+
this._tabLabelSubscription.unsubscribe();
179+
}
145180
}
146181

147182
/**
@@ -165,6 +200,16 @@ export class MdTabGroup {
165200
return event;
166201
}
167202

203+
private _subscribeToTabLabels() {
204+
if (this._tabLabelSubscription) {
205+
this._tabLabelSubscription.unsubscribe();
206+
}
207+
208+
this._tabLabelSubscription = merge(...this._tabs.map(tab => tab._labelChange)).subscribe(() => {
209+
this._changeDetectorRef.markForCheck();
210+
});
211+
}
212+
168213
/** Returns a unique id for each tab label element */
169214
_getTabLabelId(i: number): string {
170215
return `md-tab-label-${this._groupId}-${i}`;

src/lib/tabs/tab-header.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
OnDestroy,
2323
NgZone,
2424
Renderer2,
25+
ChangeDetectionStrategy,
26+
ChangeDetectorRef,
2527
} from '@angular/core';
2628
import {
2729
RIGHT_ARROW,
@@ -66,6 +68,7 @@ const EXAGGERATED_OVERSCROLL = 60;
6668
templateUrl: 'tab-header.html',
6769
styleUrls: ['tab-header.css'],
6870
encapsulation: ViewEncapsulation.None,
71+
changeDetection: ChangeDetectionStrategy.OnPush,
6972
host: {
7073
'class': 'mat-tab-header',
7174
'[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
@@ -74,7 +77,6 @@ const EXAGGERATED_OVERSCROLL = 60;
7477
})
7578
export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy {
7679
@ContentChildren(MdTabLabelWrapper) _labelWrappers: QueryList<MdTabLabelWrapper>;
77-
7880
@ViewChild(MdInkBar) _inkBar: MdInkBar;
7981
@ViewChild('tabListContainer') _tabListContainer: ElementRef;
8082
@ViewChild('tabList') _tabList: ElementRef;
@@ -137,13 +139,15 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
137139
private _elementRef: ElementRef,
138140
private _ngZone: NgZone,
139141
private _renderer: Renderer2,
142+
private _changeDetectorRef: ChangeDetectorRef,
140143
@Optional() private _dir: Directionality) { }
141144

142145
ngAfterContentChecked(): void {
143146
// If the number of tab labels have changed, check if scrolling should be enabled
144147
if (this._tabLabelCount != this._labelWrappers.length) {
145148
this._updatePagination();
146149
this._tabLabelCount = this._labelWrappers.length;
150+
this._changeDetectorRef.markForCheck();
147151
}
148152

149153
// If the selected index has changed, scroll to the label and check if the scrolling controls
@@ -153,13 +157,15 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
153157
this._checkScrollingControls();
154158
this._alignInkBarToSelectedTab();
155159
this._selectedIndexChanged = false;
160+
this._changeDetectorRef.markForCheck();
156161
}
157162

158163
// If the scroll distance has been changed (tab selected, focused, scroll controls activated),
159164
// then translate the header to reflect this.
160165
if (this._scrollDistanceChanged) {
161166
this._updateTabScrollPosition();
162167
this._scrollDistanceChanged = false;
168+
this._changeDetectorRef.markForCheck();
163169
}
164170
}
165171

@@ -207,6 +213,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
207213
_onContentChanges() {
208214
this._updatePagination();
209215
this._alignInkBarToSelectedTab();
216+
this._changeDetectorRef.markForCheck();
210217
}
211218

212219
/**
@@ -224,7 +231,6 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
224231

225232
this._focusIndex = value;
226233
this.indexFocused.emit(value);
227-
228234
this._setTabFocus(value);
229235
}
230236

@@ -259,6 +265,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
259265
// should be the full width minus the offset width.
260266
const containerEl = this._tabListContainer.nativeElement;
261267
const dir = this._getLayoutDirection();
268+
262269
if (dir == 'ltr') {
263270
containerEl.scrollLeft = 0;
264271
} else {
@@ -274,6 +281,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
274281
_moveFocus(offset: number) {
275282
if (this._labelWrappers) {
276283
const tabs: MdTabLabelWrapper[] = this._labelWrappers.toArray();
284+
277285
for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) {
278286
if (this._isValidIndex(i)) {
279287
this.focusIndex = i;
@@ -314,7 +322,6 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
314322
// Mark that the scroll distance has changed so that after the view is checked, the CSS
315323
// transformation can move the header.
316324
this._scrollDistanceChanged = true;
317-
318325
this._checkScrollingControls();
319326
}
320327
get scrollDistance(): number { return this._scrollDistance; }
@@ -341,9 +348,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
341348
* should be called sparingly.
342349
*/
343350
_scrollToLabel(labelIndex: number) {
344-
const selectedLabel = this._labelWrappers
345-
? this._labelWrappers.toArray()[labelIndex]
346-
: null;
351+
const selectedLabel = this._labelWrappers ? this._labelWrappers.toArray()[labelIndex] : null;
347352

348353
if (!selectedLabel) { return; }
349354

@@ -386,6 +391,8 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
386391
if (!this._showPaginationControls) {
387392
this.scrollDistance = 0;
388393
}
394+
395+
this._changeDetectorRef.markForCheck();
389396
}
390397

391398
/**
@@ -401,6 +408,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
401408
// Check if the pagination arrows should be activated.
402409
this._disableScrollBefore = this.scrollDistance == 0;
403410
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
411+
this._changeDetectorRef.markForCheck();
404412
}
405413

406414
/**

src/lib/tabs/tab-nav-bar/tab-nav-bar.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import {
1818
OnDestroy,
1919
Optional,
2020
ViewChild,
21-
ViewEncapsulation
21+
ViewEncapsulation,
22+
ChangeDetectionStrategy,
23+
ChangeDetectorRef,
2224
} from '@angular/core';
2325
import {MdInkBar} from '../ink-bar';
2426
import {CanDisable, mixinDisabled} from '../../core/common-behaviors/disabled';
@@ -43,6 +45,7 @@ import {fromEvent} from 'rxjs/observable/fromEvent';
4345
styleUrls: ['tab-nav-bar.css'],
4446
host: {'class': 'mat-tab-nav-bar'},
4547
encapsulation: ViewEncapsulation.None,
48+
changeDetection: ChangeDetectionStrategy.OnPush,
4649
})
4750
export class MdTabNav implements AfterContentInit, OnDestroy {
4851
/** Subject that emits when the component has been destroyed. */
@@ -56,12 +59,19 @@ export class MdTabNav implements AfterContentInit, OnDestroy {
5659
/** Subscription for window.resize event **/
5760
private _resizeSubscription: Subscription;
5861

59-
constructor(@Optional() private _dir: Directionality, private _ngZone: NgZone) { }
62+
constructor(
63+
@Optional() private _dir: Directionality,
64+
private _ngZone: NgZone,
65+
private _changeDetectorRef: ChangeDetectorRef) { }
6066

6167
/** Notifies the component that the active link has been changed. */
6268
updateActiveLink(element: ElementRef) {
6369
this._activeLinkChanged = this._activeLinkElement != element;
6470
this._activeLinkElement = element;
71+
72+
if (this._activeLinkChanged) {
73+
this._changeDetectorRef.markForCheck();
74+
}
6575
}
6676

6777
ngAfterContentInit(): void {

src/lib/tabs/tab.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
import {TemplatePortal} from '../core/portal/portal';
1010
import {
1111
ViewContainerRef, Input, TemplateRef, ViewChild, OnInit, ContentChild,
12-
Component
12+
Component, ChangeDetectionStrategy, OnDestroy, OnChanges, SimpleChanges,
1313
} from '@angular/core';
1414
import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled';
1515
import {MdTabLabel} from './tab-label';
16+
import {Subject} from 'rxjs/Subject';
1617

1718
// Boilerplate for applying mixins to MdTab.
1819
/** @docs-private */
@@ -23,9 +24,10 @@ export const _MdTabMixinBase = mixinDisabled(MdTabBase);
2324
moduleId: module.id,
2425
selector: 'md-tab, mat-tab',
2526
templateUrl: 'tab.html',
26-
inputs: ['disabled']
27+
inputs: ['disabled'],
28+
changeDetection: ChangeDetectionStrategy.OnPush,
2729
})
28-
export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
30+
export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChanges, OnDestroy {
2931
/** Content for the tab label given by <ng-template md-tab-label>. */
3032
@ContentChild(MdTabLabel) templateLabel: MdTabLabel;
3133

@@ -39,6 +41,9 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
3941
private _contentPortal: TemplatePortal | null = null;
4042
get content(): TemplatePortal | null { return this._contentPortal; }
4143

44+
/** Emits whenever the label changes. */
45+
_labelChange = new Subject<void>();
46+
4247
/**
4348
* The relatively indexed position where 0 represents the center, negative is left, and positive
4449
* represents the right.
@@ -55,6 +60,16 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
5560
super();
5661
}
5762

63+
ngOnChanges(changes: SimpleChanges) {
64+
if (changes.hasOwnProperty('textLabel')) {
65+
this._labelChange.next();
66+
}
67+
}
68+
69+
ngOnDestroy() {
70+
this._labelChange.complete();
71+
}
72+
5873
ngOnInit() {
5974
this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
6075
}

0 commit comments

Comments
 (0)