Skip to content

Commit 1c5c8d0

Browse files
committed
feat(observe-content): allow for the MutationObserver to be disabled
Adds the ability for users to disable the underlying `MutationObserver` inside the `CdkObserveContent` directive. This can be useful in the cases where it might be expensive to continue observing an element while it is invisible (e.g. an item inside of a closed dropdown).
1 parent 2436acd commit 1c5c8d0

File tree

2 files changed

+77
-13
lines changed

2 files changed

+77
-13
lines changed

src/cdk/observers/observe-content.spec.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ describe('Observe content', () => {
2222

2323
// If the hint label is empty, expect no label.
2424
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
25-
expect(spy.calls.any()).toBe(true);
25+
expect(spy).toHaveBeenCalled();
2626
done();
2727
});
2828

29-
expect(spy.calls.any()).toBe(false);
29+
expect(spy).not.toHaveBeenCalled();
3030

3131
fixture.componentInstance.text = 'text';
3232
fixture.detectChanges();
@@ -38,15 +38,43 @@ describe('Observe content', () => {
3838

3939
// If the hint label is empty, expect no label.
4040
const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
41-
expect(spy.calls.any()).toBe(true);
41+
expect(spy).toHaveBeenCalled();
4242
done();
4343
});
4444

45-
expect(spy.calls.any()).toBe(false);
45+
expect(spy).not.toHaveBeenCalled();
4646

4747
fixture.componentInstance.text = 'text';
4848
fixture.detectChanges();
4949
});
50+
51+
it('should disconnect the MutationObserver when the directive is disabled', () => {
52+
const observeSpy = jasmine.createSpy('observe spy');
53+
const disconnectSpy = jasmine.createSpy('disconnect spy');
54+
55+
// Note: since we can't know exactly when the native MutationObserver will emit, we can't
56+
// test this scenario reliably without risking flaky tests, which is why we supply a mock
57+
// MutationObserver and check that the methods are called at the right time.
58+
TestBed.overrideProvider(MutationObserverFactory, {
59+
deps: [],
60+
useFactory: () => ({
61+
create: () => ({observe: observeSpy, disconnect: disconnectSpy})
62+
})
63+
});
64+
65+
const fixture = TestBed.createComponent(ComponentWithTextContent);
66+
fixture.detectChanges();
67+
68+
expect(observeSpy).toHaveBeenCalledTimes(1);
69+
expect(disconnectSpy).not.toHaveBeenCalled();
70+
71+
fixture.componentInstance.disabled = true;
72+
fixture.detectChanges();
73+
74+
expect(observeSpy).toHaveBeenCalledTimes(1);
75+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
76+
});
77+
5078
});
5179

5280
describe('debounced', () => {
@@ -93,9 +121,16 @@ describe('Observe content', () => {
93121
});
94122

95123

96-
@Component({ template: `<div (cdkObserveContent)="doSomething()">{{text}}</div>` })
124+
@Component({
125+
template: `
126+
<div
127+
(cdkObserveContent)="doSomething()"
128+
[cdkObserveContentDisabled]="disabled">{{text}}</div>
129+
`
130+
})
97131
class ComponentWithTextContent {
98132
text = '';
133+
disabled = false;
99134
doSomething() {}
100135
}
101136

src/cdk/observers/observe-content.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
AfterContentInit,
1818
Injectable,
1919
NgZone,
20+
OnChanges,
21+
SimpleChanges,
2022
} from '@angular/core';
23+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
2124
import {Subject} from 'rxjs/Subject';
2225
import {debounceTime} from 'rxjs/operators/debounceTime';
2326

@@ -40,12 +43,23 @@ export class MutationObserverFactory {
4043
selector: '[cdkObserveContent]',
4144
exportAs: 'cdkObserveContent',
4245
})
43-
export class CdkObserveContent implements AfterContentInit, OnDestroy {
46+
export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy {
4447
private _observer: MutationObserver | null;
48+
private _disabled = false;
4549

4650
/** Event emitted for each change in the element's content. */
4751
@Output('cdkObserveContent') event = new EventEmitter<MutationRecord[]>();
4852

53+
/**
54+
* Whether observing content is disabled. This option can be used
55+
* to disconnect the underlying MutationObserver until it is needed.
56+
*/
57+
@Input('cdkObserveContentDisabled')
58+
get disabled() { return this._disabled; }
59+
set disabled(value: any) {
60+
this._disabled = coerceBooleanProperty(value);
61+
}
62+
4963
/** Used for debouncing the emitted values to the observeContent event. */
5064
private _debouncer = new Subject<MutationRecord[]>();
5165

@@ -73,21 +87,36 @@ export class CdkObserveContent implements AfterContentInit, OnDestroy {
7387
});
7488
});
7589

76-
if (this._observer) {
77-
this._observer.observe(this._elementRef.nativeElement, {
78-
'characterData': true,
79-
'childList': true,
80-
'subtree': true
81-
});
90+
if (!this.disabled) {
91+
this._enable();
92+
}
93+
}
94+
95+
ngOnChanges(changes: SimpleChanges) {
96+
if (changes['disabled']) {
97+
changes['disabled'].currentValue ? this._disable() : this._enable();
8298
}
8399
}
84100

85101
ngOnDestroy() {
102+
this._disable();
103+
this._debouncer.complete();
104+
}
105+
106+
private _disable() {
86107
if (this._observer) {
87108
this._observer.disconnect();
88109
}
110+
}
89111

90-
this._debouncer.complete();
112+
private _enable() {
113+
if (this._observer) {
114+
this._observer.observe(this._elementRef.nativeElement, {
115+
characterData: true,
116+
childList: true,
117+
subtree: true
118+
});
119+
}
91120
}
92121
}
93122

0 commit comments

Comments
 (0)