diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index ec3a42bb9feb..1057dda318bf 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -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', }, diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 1e1b6d07ceef..c9e53728ffcf 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -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; @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 { @@ -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}`; diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index f7c323c857c7..3de08afdf9ef 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -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; - @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(); } /** diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index b0a3f56c3afe..b292b7bf67ef 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -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 { diff --git a/src/lib/tabs/tab.ts b/src/lib/tabs/tab.ts index b0ef3b7c1cb2..b207f9736ba0 100644 --- a/src/lib/tabs/tab.ts +++ b/src/lib/tabs/tab.ts @@ -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 . */ @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(); + /** * 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); }