Skip to content

Commit 7ab4430

Browse files
EladBezaleljelbourn
authored andcommitted
fix(tabs): observing tab header label changes to recalculate width (#2186)
- Uses observe-changes directive that emit an event when the mutation observer notifies a change fixes #2155
1 parent f45c315 commit 7ab4430

File tree

7 files changed

+140
-5
lines changed

7 files changed

+140
-5
lines changed

src/lib/core/core.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {NgModule, ModuleWithProviders} from '@angular/core';
22
import {MdLineModule} from './line/line';
33
import {RtlModule} from './rtl/dir';
4+
import {ObserveContentModule} from './observe-content/observe-content';
45
import {MdRippleModule} from './ripple/ripple';
56
import {PortalModule} from './portal/portal-directives';
67
import {OverlayModule} from './overlay/overlay-directives';
@@ -11,6 +12,9 @@ import {OVERLAY_PROVIDERS} from './overlay/overlay';
1112
// RTL
1213
export {Dir, LayoutDirection, RtlModule} from './rtl/dir';
1314

15+
// Mutation Observer
16+
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
17+
1418
// Portals
1519
export {
1620
Portal,
@@ -115,6 +119,7 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode
115119
MdLineModule,
116120
RtlModule,
117121
MdRippleModule,
122+
ObserveContentModule,
118123
PortalModule,
119124
OverlayModule,
120125
A11yModule,
@@ -123,6 +128,7 @@ export {NoConflictStyleCompatibilityMode} from './compatibility/no-conflict-mode
123128
MdLineModule,
124129
RtlModule,
125130
MdRippleModule,
131+
ObserveContentModule,
126132
PortalModule,
127133
OverlayModule,
128134
A11yModule,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {Component} from '@angular/core';
2+
import {async, TestBed} from '@angular/core/testing';
3+
import {ObserveContentModule} from './observe-content';
4+
5+
/**
6+
* TODO(elad): `ProxyZone` doesn't seem to capture the events raised by
7+
* `MutationObserver` and needs to be investigated
8+
*/
9+
10+
describe('Observe content', () => {
11+
beforeEach(async(() => {
12+
TestBed.configureTestingModule({
13+
imports: [ObserveContentModule],
14+
declarations: [ComponentWithTextContent, ComponentWithChildTextContent],
15+
});
16+
17+
TestBed.compileComponents();
18+
}));
19+
20+
describe('text content change', () => {
21+
it('should call the registered for changes function', done => {
22+
let fixture = TestBed.createComponent(ComponentWithTextContent);
23+
fixture.detectChanges();
24+
25+
// If the hint label is empty, expect no label.
26+
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
27+
expect(spy.calls.any()).toBe(true);
28+
done();
29+
});
30+
31+
expect(spy.calls.any()).toBe(false);
32+
33+
fixture.componentInstance.text = 'text';
34+
fixture.detectChanges();
35+
});
36+
});
37+
38+
describe('child text content change', () => {
39+
it('should call the registered for changes function', done => {
40+
let fixture = TestBed.createComponent(ComponentWithChildTextContent);
41+
fixture.detectChanges();
42+
43+
// If the hint label is empty, expect no label.
44+
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
45+
expect(spy.calls.any()).toBe(true);
46+
done();
47+
});
48+
49+
expect(spy.calls.any()).toBe(false);
50+
51+
fixture.componentInstance.text = 'text';
52+
fixture.detectChanges();
53+
});
54+
});
55+
});
56+
57+
58+
@Component({ template: `<div (cdkObserveContent)="doSomething()">{{text}}</div>` })
59+
class ComponentWithTextContent {
60+
text = '';
61+
doSomething() {}
62+
}
63+
64+
@Component({ template: `<div (cdkObserveContent)="doSomething()"><div>{{text}}<div></div>` })
65+
class ComponentWithChildTextContent {
66+
text = '';
67+
doSomething() {}
68+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
Directive,
3+
ElementRef,
4+
NgModule,
5+
ModuleWithProviders,
6+
Output,
7+
EventEmitter,
8+
OnDestroy,
9+
AfterContentInit
10+
} from '@angular/core';
11+
12+
@Directive({
13+
selector: '[cdkObserveContent]'
14+
})
15+
export class ObserveContent implements AfterContentInit, OnDestroy {
16+
private _observer: MutationObserver;
17+
18+
@Output('cdkObserveContent') event = new EventEmitter<void>();
19+
20+
constructor(private _elementRef: ElementRef) {}
21+
22+
ngAfterContentInit() {
23+
this._observer = new MutationObserver(mutations => mutations.forEach(() => this.event.emit()));
24+
25+
this._observer.observe(this._elementRef.nativeElement, {
26+
characterData: true,
27+
childList: true,
28+
subtree: true
29+
});
30+
}
31+
32+
ngOnDestroy() {
33+
if (this._observer) {
34+
this._observer.disconnect();
35+
}
36+
}
37+
}
38+
39+
@NgModule({
40+
exports: [ObserveContent],
41+
declarations: [ObserveContent]
42+
})
43+
export class ObserveContentModule {
44+
static forRoot(): ModuleWithProviders {
45+
return {
46+
ngModule: ObserveContentModule,
47+
providers: []
48+
};
49+
}
50+
}

src/lib/module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {NgModule, ModuleWithProviders} from '@angular/core';
33
import {
44
MdRippleModule,
55
RtlModule,
6+
ObserveContentModule,
67
PortalModule,
78
OverlayModule,
89
A11yModule,
@@ -67,6 +68,7 @@ const MATERIAL_MODULES = [
6768
PlatformModule,
6869
ProjectionModule,
6970
DefaultStyleCompatibilityModeModule,
71+
ObserveContentModule
7072
];
7173

7274
@NgModule({
@@ -89,6 +91,7 @@ const MATERIAL_MODULES = [
8991
PortalModule.forRoot(),
9092
ProjectionModule.forRoot(),
9193
RtlModule.forRoot(),
94+
ObserveContentModule.forRoot(),
9295

9396
// These modules include providers.
9497
A11yModule.forRoot(),

src/lib/tabs/tab-group.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {MdInkBar} from './ink-bar';
2323
import {Observable} from 'rxjs/Observable';
2424
import 'rxjs/add/operator/map';
2525
import {MdRippleModule} from '../core/ripple/ripple';
26+
import {ObserveContentModule} from '../core/observe-content/observe-content';
2627
import {MdTab} from './tab';
2728
import {MdTabBody} from './tab-body';
2829
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
@@ -194,7 +195,7 @@ export class MdTabGroup {
194195
}
195196

196197
@NgModule({
197-
imports: [CommonModule, PortalModule, MdRippleModule],
198+
imports: [CommonModule, PortalModule, MdRippleModule, ObserveContentModule],
198199
// Don't export all components because some are only to be used internally.
199200
exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple],
200201
declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper,

src/lib/tabs/tab-header.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<div class="md-tab-label-container" #tabListContainer
1010
(keydown)="_handleKeydown($event)">
11-
<div class="md-tab-list" #tabList role="tablist">
11+
<div class="md-tab-list" #tabList role="tablist" (cdkObserveContent)="_updatePagination()">
1212
<ng-content></ng-content>
1313
<md-ink-bar></md-ink-bar>
1414
</div>

src/lib/tabs/tab-header.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ export class MdTabHeader {
101101
ngAfterContentChecked(): void {
102102
// If the number of tab labels have changed, check if scrolling should be enabled
103103
if (this._tabLabelCount != this._labelWrappers.length) {
104-
this._checkPaginationEnabled();
105-
this._checkScrollingControls();
106-
this._updateTabScrollPosition();
104+
this._updatePagination();
107105
this._tabLabelCount = this._labelWrappers.length;
108106
}
109107

@@ -150,6 +148,15 @@ export class MdTabHeader {
150148
}
151149
}
152150

151+
/**
152+
* Updating the view whether pagination should be enabled or not
153+
*/
154+
_updatePagination() {
155+
this._checkPaginationEnabled();
156+
this._checkScrollingControls();
157+
this._updateTabScrollPosition();
158+
}
159+
153160
/** When the focus index is set, we must manually send focus to the correct label */
154161
set focusIndex(value: number) {
155162
if (!this._isValidIndex(value) || this._focusIndex == value) { return; }

0 commit comments

Comments
 (0)