chore(tabs): switch to OnPush change detection (#5631)

Switches all of the tab-related components to OnPush change detection and sorts out the issues that come from the changes.

Relates to #5035.
This commit is contained in:
Kristiyan Kostadinov
2017-07-11 19:18:41 +02:00
committed by Jeremy Elbourn
parent 70117ff46d
commit ed2ece9e4f
5 changed files with 108 additions and 19 deletions
+2
View File
@@ -17,6 +17,7 @@ import {
Optional,
AfterViewChecked,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import {
trigger,
@@ -59,6 +60,7 @@ export type MdTabBodyOriginState = 'left' | 'right';
templateUrl: 'tab-body.html',
styleUrls: ['tab-body.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'mat-tab-body',
},
+62 -8
View File
@@ -16,11 +16,19 @@ import {
ContentChildren,
ElementRef,
Renderer2,
ChangeDetectionStrategy,
ChangeDetectorRef,
AfterViewChecked,
AfterContentInit,
AfterContentChecked,
OnDestroy,
} from '@angular/core';
import {coerceBooleanProperty} from '../core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import {MdTab} from './tab';
import {map} from '../core/rxjs/index';
import {merge} from 'rxjs/observable/merge';
/** Used to generate unique ID's for each tab component */
@@ -45,13 +53,16 @@ export type MdTabHeaderPosition = 'above' | 'below';
selector: 'md-tab-group, mat-tab-group',
templateUrl: 'tab-group.html',
styleUrls: ['tab-group.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'mat-tab-group',
'[class.mat-tab-group-dynamic-height]': 'dynamicHeight',
'[class.mat-tab-group-inverted-header]': 'headerPosition === "below"',
}
})
export class MdTabGroup {
export class MdTabGroup implements AfterContentInit, AfterContentChecked,
AfterViewChecked, OnDestroy {
@ContentChildren(MdTab) _tabs: QueryList<MdTab>;
@ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef;
@@ -65,6 +76,12 @@ export class MdTabGroup {
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
private _tabBodyWrapperHeight: number = 0;
/** Subscription to tabs being added/removed. */
private _tabsSubscription: Subscription;
/** Subscription to changes in the tab labels. */
private _tabLabelSubscription: Subscription;
/** Whether the tab group should grow to the size of the active tab. */
@Input()
get dynamicHeight(): boolean { return this._dynamicHeight; }
@@ -82,17 +99,14 @@ export class MdTabGroup {
set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); }
private _disableRipple: boolean = false;
private _selectedIndex: number | null = null;
/** The index of the active tab. */
@Input()
set selectedIndex(value: number | null) { this._indexToSelect = value; }
get selectedIndex(): number | null { return this._selectedIndex; }
private _selectedIndex: number | null = null;
/** Position of the tab header. */
@Input()
headerPosition: MdTabHeaderPosition = 'above';
@Input() headerPosition: MdTabHeaderPosition = 'above';
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
@Output() get selectedIndexChange(): Observable<number> {
@@ -107,7 +121,7 @@ export class MdTabGroup {
private _groupId: number;
constructor(private _renderer: Renderer2) {
constructor(private _renderer: Renderer2, private _changeDetectorRef: ChangeDetectorRef) {
this._groupId = nextId++;
}
@@ -141,7 +155,31 @@ export class MdTabGroup {
}
});
this._selectedIndex = indexToSelect;
if (this._selectedIndex !== indexToSelect) {
this._selectedIndex = indexToSelect;
this._changeDetectorRef.markForCheck();
}
}
ngAfterContentInit() {
this._subscribeToTabLabels();
// Subscribe to changes in the amount of tabs, in order to be
// able to re-render the content as new tabs are added or removed.
this._tabsSubscription = this._tabs.changes.subscribe(() => {
this._subscribeToTabLabels();
this._changeDetectorRef.markForCheck();
});
}
ngOnDestroy() {
if (this._tabsSubscription) {
this._tabsSubscription.unsubscribe();
}
if (this._tabLabelSubscription) {
this._tabLabelSubscription.unsubscribe();
}
}
/**
@@ -165,6 +203,22 @@ export class MdTabGroup {
return event;
}
/**
* Subscribes to changes in the tab labels. This is needed, because the @Input for the label is
* on the MdTab component, whereas the data binding is inside the MdTabGroup. In order for the
* binding to be updated, we need to subscribe to changes in it and trigger change detection
* manually.
*/
private _subscribeToTabLabels() {
if (this._tabLabelSubscription) {
this._tabLabelSubscription.unsubscribe();
}
this._tabLabelSubscription = merge(...this._tabs.map(tab => tab._labelChange)).subscribe(() => {
this._changeDetectorRef.markForCheck();
});
}
/** Returns a unique id for each tab label element */
_getTabLabelId(i: number): string {
return `md-tab-label-${this._groupId}-${i}`;
+14 -6
View File
@@ -22,6 +22,8 @@ import {
OnDestroy,
NgZone,
Renderer2,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {
RIGHT_ARROW,
@@ -66,6 +68,7 @@ const EXAGGERATED_OVERSCROLL = 60;
templateUrl: 'tab-header.html',
styleUrls: ['tab-header.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'mat-tab-header',
'[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
@@ -74,7 +77,6 @@ const EXAGGERATED_OVERSCROLL = 60;
})
export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy {
@ContentChildren(MdTabLabelWrapper) _labelWrappers: QueryList<MdTabLabelWrapper>;
@ViewChild(MdInkBar) _inkBar: MdInkBar;
@ViewChild('tabListContainer') _tabListContainer: ElementRef;
@ViewChild('tabList') _tabList: ElementRef;
@@ -137,6 +139,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
private _elementRef: ElementRef,
private _ngZone: NgZone,
private _renderer: Renderer2,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir: Directionality) { }
ngAfterContentChecked(): void {
@@ -144,6 +147,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
if (this._tabLabelCount != this._labelWrappers.length) {
this._updatePagination();
this._tabLabelCount = this._labelWrappers.length;
this._changeDetectorRef.markForCheck();
}
// If the selected index has changed, scroll to the label and check if the scrolling controls
@@ -153,6 +157,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
this._checkScrollingControls();
this._alignInkBarToSelectedTab();
this._selectedIndexChanged = false;
this._changeDetectorRef.markForCheck();
}
// If the scroll distance has been changed (tab selected, focused, scroll controls activated),
@@ -160,6 +165,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
if (this._scrollDistanceChanged) {
this._updateTabScrollPosition();
this._scrollDistanceChanged = false;
this._changeDetectorRef.markForCheck();
}
}
@@ -207,6 +213,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
_onContentChanges() {
this._updatePagination();
this._alignInkBarToSelectedTab();
this._changeDetectorRef.markForCheck();
}
/**
@@ -224,7 +231,6 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
this._focusIndex = value;
this.indexFocused.emit(value);
this._setTabFocus(value);
}
@@ -259,6 +265,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
// should be the full width minus the offset width.
const containerEl = this._tabListContainer.nativeElement;
const dir = this._getLayoutDirection();
if (dir == 'ltr') {
containerEl.scrollLeft = 0;
} else {
@@ -274,6 +281,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
_moveFocus(offset: number) {
if (this._labelWrappers) {
const tabs: MdTabLabelWrapper[] = this._labelWrappers.toArray();
for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) {
if (this._isValidIndex(i)) {
this.focusIndex = i;
@@ -314,7 +322,6 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
// Mark that the scroll distance has changed so that after the view is checked, the CSS
// transformation can move the header.
this._scrollDistanceChanged = true;
this._checkScrollingControls();
}
get scrollDistance(): number { return this._scrollDistance; }
@@ -341,9 +348,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
* should be called sparingly.
*/
_scrollToLabel(labelIndex: number) {
const selectedLabel = this._labelWrappers
? this._labelWrappers.toArray()[labelIndex]
: null;
const selectedLabel = this._labelWrappers ? this._labelWrappers.toArray()[labelIndex] : null;
if (!selectedLabel) { return; }
@@ -386,6 +391,8 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
if (!this._showPaginationControls) {
this.scrollDistance = 0;
}
this._changeDetectorRef.markForCheck();
}
/**
@@ -401,6 +408,7 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
// Check if the pagination arrows should be activated.
this._disableScrollBefore = this.scrollDistance == 0;
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
this._changeDetectorRef.markForCheck();
}
/**
+12 -2
View File
@@ -18,7 +18,9 @@ import {
OnDestroy,
Optional,
ViewChild,
ViewEncapsulation
ViewEncapsulation,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {MdInkBar} from '../ink-bar';
import {CanDisable, mixinDisabled} from '../../core/common-behaviors/disabled';
@@ -43,6 +45,7 @@ import {fromEvent} from 'rxjs/observable/fromEvent';
styleUrls: ['tab-nav-bar.css'],
host: {'class': 'mat-tab-nav-bar'},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdTabNav implements AfterContentInit, OnDestroy {
/** Subject that emits when the component has been destroyed. */
@@ -56,12 +59,19 @@ export class MdTabNav implements AfterContentInit, OnDestroy {
/** Subscription for window.resize event **/
private _resizeSubscription: Subscription;
constructor(@Optional() private _dir: Directionality, private _ngZone: NgZone) { }
constructor(
@Optional() private _dir: Directionality,
private _ngZone: NgZone,
private _changeDetectorRef: ChangeDetectorRef) { }
/** Notifies the component that the active link has been changed. */
updateActiveLink(element: ElementRef) {
this._activeLinkChanged = this._activeLinkElement != element;
this._activeLinkElement = element;
if (this._activeLinkChanged) {
this._changeDetectorRef.markForCheck();
}
}
ngAfterContentInit(): void {
+18 -3
View File
@@ -9,10 +9,11 @@
import {TemplatePortal} from '../core/portal/portal';
import {
ViewContainerRef, Input, TemplateRef, ViewChild, OnInit, ContentChild,
Component
Component, ChangeDetectionStrategy, OnDestroy, OnChanges, SimpleChanges,
} from '@angular/core';
import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled';
import {MdTabLabel} from './tab-label';
import {Subject} from 'rxjs/Subject';
// Boilerplate for applying mixins to MdTab.
/** @docs-private */
@@ -23,9 +24,10 @@ export const _MdTabMixinBase = mixinDisabled(MdTabBase);
moduleId: module.id,
selector: 'md-tab, mat-tab',
templateUrl: 'tab.html',
inputs: ['disabled']
inputs: ['disabled'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable, OnChanges, OnDestroy {
/** Content for the tab label given by <ng-template md-tab-label>. */
@ContentChild(MdTabLabel) templateLabel: MdTabLabel;
@@ -39,6 +41,9 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
private _contentPortal: TemplatePortal | null = null;
get content(): TemplatePortal | null { return this._contentPortal; }
/** Emits whenever the label changes. */
_labelChange = new Subject<void>();
/**
* The relatively indexed position where 0 represents the center, negative is left, and positive
* represents the right.
@@ -55,6 +60,16 @@ export class MdTab extends _MdTabMixinBase implements OnInit, CanDisable {
super();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.hasOwnProperty('textLabel')) {
this._labelChange.next();
}
}
ngOnDestroy() {
this._labelChange.complete();
}
ngOnInit() {
this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
}