Skip to content

Commit 82e86d9

Browse files
committed
address comments
1 parent d5f53e1 commit 82e86d9

File tree

2 files changed

+79
-63
lines changed

2 files changed

+79
-63
lines changed

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {Component, ElementRef, ViewChild} from '@angular/core';
22
import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
3-
import {ContentObserverFactory, MutationObserverFactory, ObserversModule} from './observe-content';
3+
import {ContentObserver, MutationObserverFactory, ObserversModule} from './observe-content';
44

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

8-
describe('Observe content directive', () => {
8+
fdescribe('Observe content directive', () => {
99
describe('basic usage', () => {
1010
beforeEach(async(() => {
1111
TestBed.configureTestingModule({
@@ -120,7 +120,7 @@ describe('Observe content directive', () => {
120120
});
121121
});
122122

123-
describe('Observe content injectable', () => {
123+
fdescribe('ContentObserver injectable', () => {
124124
describe('basic usage', () => {
125125
let callbacks: Function[];
126126
let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));
@@ -150,15 +150,14 @@ describe('Observe content injectable', () => {
150150
}));
151151

152152
it('should trigger the callback when the content of the element changes',
153-
fakeAsync(inject([ContentObserverFactory], (cof: ContentObserverFactory) => {
153+
fakeAsync(inject([ContentObserver], (co: ContentObserver) => {
154154
let fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
155155
fixture.detectChanges();
156156

157157
const spy = jasmine.createSpy('content observer');
158158

159-
const observer = cof.create(fixture.componentInstance.contentEl.nativeElement);
160-
observer.changes.subscribe(() => spy());
161-
observer.start();
159+
const subscription = co.observe(fixture.componentInstance.contentEl.nativeElement)
160+
.subscribe(() => spy());
162161

163162
expect(spy).not.toHaveBeenCalled();
164163

@@ -167,6 +166,8 @@ describe('Observe content injectable', () => {
167166
invokeCallbacks();
168167

169168
expect(spy).toHaveBeenCalled();
169+
170+
subscription.unsubscribe();
170171
})));
171172
});
172173
});

src/cdk/observers/observe-content.ts

Lines changed: 71 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ import {
1515
Injectable,
1616
Input,
1717
NgModule,
18-
NgZone,
1918
OnDestroy,
2019
Output,
2120
} from '@angular/core';
22-
import {Observable, Subject} from 'rxjs';
21+
import {Observable, Subject, Subscription} from 'rxjs';
2322
import {debounceTime} from 'rxjs/operators';
2423

2524
/**
@@ -36,49 +35,63 @@ export class MutationObserverFactory {
3635

3736
/** A factory that creates ContentObservers. */
3837
@Injectable({providedIn: 'root'})
39-
export class ContentObserverFactory {
40-
constructor(private _mutationObserverFactory: MutationObserverFactory, private _ngZone: NgZone) {}
41-
42-
create(element: Element, debounce?: number) {
43-
const changes = new Subject<MutationRecord[]>();
44-
const observer = this._ngZone.runOutsideAngular(
45-
() => this._mutationObserverFactory.create((mutations) => changes.next(mutations)));
46-
return new ContentObserver(element, observer, changes, debounce);
47-
}
48-
}
49-
50-
51-
/** A class that observes an element for content changes. */
5238
export class ContentObserver {
53-
changes: Observable<MutationRecord[]>;
54-
55-
constructor(private _element: Element, private _mutationObserver: MutationObserver | null,
56-
private _rawChanges: Subject<MutationRecord[]>, debounce: number = 0) {
57-
this.changes = debounce ?
58-
this._rawChanges.pipe(debounceTime(debounce)) : this._rawChanges.asObservable();
39+
private _observedElements = new Map<Element, {
40+
observer: MutationObserver | null,
41+
stream: Subject<MutationRecord[]>,
42+
count: number
43+
}>();
44+
45+
constructor(private _mutationObserverFactory: MutationObserverFactory) {}
46+
47+
observe(element: Element, debounce?: number): Observable<MutationRecord[]> {
48+
return Observable.create(observer => {
49+
const stream = this._observeElement(element);
50+
const subscription =
51+
(debounce ? stream.pipe(debounceTime(debounce)) : stream).subscribe(observer);
52+
53+
return () => {
54+
subscription.unsubscribe();
55+
this._unobserveElement(element);
56+
}
57+
});
5958
}
6059

61-
start(): ContentObserver {
62-
if (this._mutationObserver) {
63-
this._mutationObserver.observe(this._element, {
64-
characterData: true,
65-
childList: true,
66-
subtree: true
67-
});
60+
private _observeElement(element: Element): Subject<MutationRecord[]> {
61+
if (!this._observedElements.has(element)) {
62+
const stream = new Subject<MutationRecord[]>();
63+
const observer = this._mutationObserverFactory.create(mutations => stream.next(mutations));
64+
if (observer) {
65+
observer.observe(element, {
66+
characterData: true,
67+
childList: true,
68+
subtree: true
69+
});
70+
}
71+
this._observedElements.set(element, {observer, stream, count: 1});
72+
} else {
73+
this._observedElements.get(element)!.count++;
6874
}
69-
return this;
75+
return this._observedElements.get(element)!.stream;
7076
}
7177

72-
pause() {
73-
if (this._mutationObserver) {
74-
this._mutationObserver.disconnect();
78+
private _unobserveElement(element: Element) {
79+
if (this._observedElements.has(element)) {
80+
if (!--this._observedElements.get(element)!.count) {
81+
this._cleanupObserver(element);
82+
}
7583
}
7684
}
7785

78-
stop() {
79-
this.pause();
80-
this._rawChanges.complete();
81-
this._mutationObserver = null;
86+
private _cleanupObserver(element: Element) {
87+
if (this._observedElements.has(element)) {
88+
const {observer, stream} = this._observedElements.get(element)!;
89+
if (observer) {
90+
observer.disconnect();
91+
}
92+
stream.complete();
93+
this._observedElements.delete(element);
94+
}
8295
}
8396
}
8497

@@ -103,12 +116,10 @@ export class CdkObserveContent implements AfterContentInit, OnDestroy {
103116
get disabled() { return this._disabled; }
104117
set disabled(value: any) {
105118
this._disabled = coerceBooleanProperty(value);
106-
if (this._observer) {
107-
if (this._disabled) {
108-
this._observer.pause();
109-
} else {
110-
this._observer.start();
111-
}
119+
if (this._disabled) {
120+
this._unsubscribe();
121+
} else {
122+
this._subscribe();
112123
}
113124
}
114125
private _disabled = false;
@@ -118,30 +129,34 @@ export class CdkObserveContent implements AfterContentInit, OnDestroy {
118129
get debounce(): number { return this._debounce; }
119130
set debounce(value: number) {
120131
this._debounce = coerceNumberProperty(value);
132+
this._subscribe();
121133
}
122134
private _debounce: number;
123135

124-
private _observer: ContentObserver;
136+
private _currentSubscription: Subscription | null = null;
125137

126-
constructor(private _contentObserverFactory: ContentObserverFactory,
127-
private _elementRef: ElementRef, private _ngZone: NgZone) {}
138+
constructor(private _contentObserver: ContentObserver, private _elementRef: ElementRef) {}
128139

129140
ngAfterContentInit() {
130-
this._observer =
131-
this._contentObserverFactory.create(this._elementRef.nativeElement, this.debounce);
132-
this._ngZone.run(
133-
() => this._observer.changes.subscribe(mutations => this.event.next(mutations)));
134-
135-
if (!this.disabled) {
136-
this._observer.start();
141+
if (!this._currentSubscription && !this.disabled) {
142+
this._subscribe();
137143
}
138144
}
139145

140146
ngOnDestroy() {
141-
// Its possible _observer is undefined if the component is destroyed before init
142-
// (could happen in a test).
143-
if (this._observer) {
144-
this._observer.stop();
147+
this._unsubscribe();
148+
}
149+
150+
private _subscribe() {
151+
this._unsubscribe();
152+
this._currentSubscription =
153+
this._contentObserver.observe(this._elementRef.nativeElement, this.debounce)
154+
.subscribe(mutations => this.event.next(mutations));
155+
}
156+
157+
private _unsubscribe() {
158+
if (this._currentSubscription) {
159+
this._currentSubscription.unsubscribe();
145160
}
146161
}
147162
}

0 commit comments

Comments
 (0)