Skip to content

Commit

Permalink
fix(tabs): observing tab header label changes to recalculate width
Browse files Browse the repository at this point in the history
- Uses observe-changes directive that emit an event when the mutation observer notifies a change

fixes angular#2155
  • Loading branch information
EladBezalel committed Dec 16, 2016
1 parent 2897b9b commit f839831
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 7 deletions.
10 changes: 8 additions & 2 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {NgModule, ModuleWithProviders} from '@angular/core';
import {MdLineModule} from './line/line';
import {RtlModule} from './rtl/dir';
import {ObserveContentModule} from './observe-content/observe-content';
import {MdRippleModule} from './ripple/ripple';
import {PortalModule} from './portal/portal-directives';
import {OverlayModule} from './overlay/overlay-directives';
Expand All @@ -11,6 +12,9 @@ import {OVERLAY_PROVIDERS} from './overlay/overlay';
// RTL
export {Dir, LayoutDirection, RtlModule} from './rtl/dir';

// Mutation Observer
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';

// Portals
export {
Portal,
Expand Down Expand Up @@ -99,8 +103,10 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode


@NgModule({
imports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
exports: [MdLineModule, RtlModule, MdRippleModule, PortalModule, OverlayModule, A11yModule],
imports: [MdLineModule, RtlModule, ObserveContentModule,
MdRippleModule, PortalModule, OverlayModule, A11yModule],
exports: [MdLineModule, RtlModule, ObserveContentModule,
MdRippleModule, PortalModule, OverlayModule, A11yModule],
})
export class MdCoreModule {
static forRoot(): ModuleWithProviders {
Expand Down
65 changes: 65 additions & 0 deletions src/lib/core/observe-content/observe-content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Component} from '@angular/core';
import {async, TestBed} from '@angular/core/testing';
import {ObserveContentModule} from './observe-content';

// TODO(elad): `ProxyZone` doesn't seem to capture the events raised by `MutationObserver` and needs to be investigated

describe('Observe content', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ObserveContentModule],
declarations: [ComponentWithTextContent, ComponentWithChildTextContent],
});

TestBed.compileComponents();
}));

describe('text content change', () => {
it('should call the registered for changes function', done => {
let fixture = TestBed.createComponent(ComponentWithTextContent);
fixture.detectChanges();

// If the hint label is empty, expect no label.
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
expect(spy.calls.any()).toBe(true);
done();
});

expect(spy.calls.any()).toBe(false);

fixture.componentInstance.text = 'text';
fixture.detectChanges();
});
});

describe('child text content change', () => {
it('should call the registered for changes function', done => {
let fixture = TestBed.createComponent(ComponentWithChildTextContent);
fixture.detectChanges();

// If the hint label is empty, expect no label.
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
expect(spy.calls.any()).toBe(true);
done();
});

expect(spy.calls.any()).toBe(false);

fixture.componentInstance.text = 'text';
fixture.detectChanges();
});
});
});


@Component({ template: `<div (cdk-observe-content)="doSomething()">{{text}}</div>` })
class ComponentWithTextContent {
text = '';
doSomething() {}
}

@Component({ template: `<div (cdk-observe-content)="doSomething()"><div>{{text}}<div></div>` })
class ComponentWithChildTextContent {
text = '';
doSomething() {}
}
48 changes: 48 additions & 0 deletions src/lib/core/observe-content/observe-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Directive,
ElementRef,
NgModule,
ModuleWithProviders,
Output,
EventEmitter,
OnDestroy,
AfterContentInit
} from '@angular/core';

@Directive({
selector: '[cdk-observe-content]'
})
export class ObserveContent implements AfterContentInit, OnDestroy {
private _observer: MutationObserver;

@Output('cdk-observe-content') event = new EventEmitter<void>();

constructor(private _elementRef: ElementRef) {}

ngAfterContentInit() {
this._observer = new MutationObserver(mutations => mutations.forEach(() => this.event.emit()));

this._observer.observe(this._elementRef.nativeElement, {
characterData: true,
childList: true,
subtree: true
});
}

ngOnDestroy() {
this._observer.disconnect();
}
}

@NgModule({
exports: [ObserveContent],
declarations: [ObserveContent]
})
export class ObserveContentModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ObserveContentModule,
providers: []
};
}
}
3 changes: 3 additions & 0 deletions src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core';
import {
MdRippleModule,
RtlModule,
ObserveContentModule,
PortalModule,
OverlayModule,
A11yModule,
Expand Down Expand Up @@ -67,6 +68,7 @@ const MATERIAL_MODULES = [
PlatformModule,
ProjectionModule,
DefaultStyleCompatibilityModeModule,
ObserveContentModule
];

@NgModule({
Expand All @@ -89,6 +91,7 @@ const MATERIAL_MODULES = [
PortalModule.forRoot(),
ProjectionModule.forRoot(),
RtlModule.forRoot(),
ObserveContentModule.forRoot(),

// These modules include providers.
A11yModule.forRoot(),
Expand Down
3 changes: 2 additions & 1 deletion src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {MdInkBar} from './ink-bar';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {MdRippleModule} from '../core/ripple/ripple';
import {ObserveContentModule} from '../core/observe-content/observe-content';
import {MdTab} from './tab';
import {MdTabBody} from './tab-body';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
Expand Down Expand Up @@ -188,7 +189,7 @@ export class MdTabGroup {
}

@NgModule({
imports: [CommonModule, PortalModule, MdRippleModule],
imports: [CommonModule, PortalModule, MdRippleModule, ObserveContentModule],
// Don't export all components because some are only to be used internally.
exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple],
declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tabs/tab-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<div class="md-tab-label-container" #tabListContainer
(keydown)="_handleKeydown($event)">
<div class="md-tab-list" #tabList role="tablist">
<div class="md-tab-list" #tabList role="tablist" (cdk-observe-content)="_updatePagination()">
<ng-content></ng-content>
<md-ink-bar></md-ink-bar>
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/lib/tabs/tab-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,7 @@ export class MdTabHeader {
ngAfterContentChecked(): void {
// If the number of tab labels have changed, check if scrolling should be enabled
if (this._tabLabelCount != this._labelWrappers.length) {
this._checkPaginationEnabled();
this._checkScrollingControls();
this._updateTabScrollPosition();
this._updatePagination();
this._tabLabelCount = this._labelWrappers.length;
}

Expand Down Expand Up @@ -150,6 +148,16 @@ export class MdTabHeader {
}
}

/**
* Updating the view whether pagination should be enabled or not
* @private
*/
_updatePagination() {
this._checkPaginationEnabled();
this._checkScrollingControls();
this._updateTabScrollPosition();
}

/** When the focus index is set, we must manually send focus to the correct label */
set focusIndex(value: number) {
if (!this._isValidIndex(value) || this._focusIndex == value) { return; }
Expand Down

0 comments on commit f839831

Please sign in to comment.