Skip to content

Commit 375bb52

Browse files
committed
feat(observe-content): refactor so logic can be used without directive
1 parent fbf06bb commit 375bb52

File tree

2 files changed

+140
-59
lines changed

2 files changed

+140
-59
lines changed

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

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {Component} from '@angular/core';
2-
import {async, TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
3-
import {ObserversModule, MutationObserverFactory} from './observe-content';
1+
import {Component, ElementRef, ViewChild} from '@angular/core';
2+
import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
3+
import {ContentObserverFactory, 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', () => {
8+
describe('Observe content directive', () => {
99
describe('basic usage', () => {
1010
beforeEach(async(() => {
1111
TestBed.configureTestingModule({
@@ -120,6 +120,57 @@ describe('Observe content', () => {
120120
});
121121
});
122122

123+
describe('Observe content injectable', () => {
124+
describe('basic usage', () => {
125+
let callbacks: Function[];
126+
let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));
127+
128+
beforeEach(fakeAsync(() => {
129+
callbacks = [];
130+
131+
TestBed.configureTestingModule({
132+
imports: [ObserversModule],
133+
declarations: [UnobservedComponentWithTextContent],
134+
providers: [{
135+
provide: MutationObserverFactory,
136+
useValue: {
137+
create: function(callback: Function) {
138+
callbacks.push(callback);
139+
140+
return {
141+
observe: () => {},
142+
disconnect: () => {}
143+
};
144+
}
145+
}
146+
}]
147+
});
148+
149+
TestBed.compileComponents();
150+
}));
151+
152+
it('should trigger the callback when the content of the element changes',
153+
fakeAsync(inject([ContentObserverFactory], (cof: ContentObserverFactory) => {
154+
let fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
155+
fixture.detectChanges();
156+
157+
const spy = jasmine.createSpy('content observer');
158+
159+
const observer = cof.create(fixture.componentInstance.contentEl.nativeElement);
160+
observer.changes.subscribe(() => spy());
161+
observer.start();
162+
163+
expect(spy).not.toHaveBeenCalled();
164+
165+
fixture.componentInstance.text = 'text';
166+
fixture.detectChanges();
167+
invokeCallbacks();
168+
169+
expect(spy).toHaveBeenCalled();
170+
})));
171+
});
172+
});
173+
123174

124175
@Component({
125176
template: `
@@ -147,3 +198,11 @@ class ComponentWithDebouncedListener {
147198
debounce = 500;
148199
spy = jasmine.createSpy('MutationObserver callback');
149200
}
201+
202+
@Component({
203+
template: `<div #contentEl>{{text}}</div>`
204+
})
205+
class UnobservedComponentWithTextContent {
206+
@ViewChild('contentEl') contentEl: ElementRef;
207+
text = '';
208+
}

src/cdk/observers/observe-content.ts

Lines changed: 77 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {coerceBooleanProperty} from '@angular/cdk/coercion';
9+
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
1010
import {
1111
AfterContentInit,
1212
Directive,
@@ -16,12 +16,10 @@ import {
1616
Input,
1717
NgModule,
1818
NgZone,
19-
OnChanges,
2019
OnDestroy,
2120
Output,
22-
SimpleChanges,
2321
} from '@angular/core';
24-
import {Subject} from 'rxjs';
22+
import {Observable, Subject} from 'rxjs';
2523
import {debounceTime} from 'rxjs/operators';
2624

2725
/**
@@ -35,6 +33,56 @@ export class MutationObserverFactory {
3533
}
3634
}
3735

36+
37+
/** A factory that creates ContentObservers. */
38+
@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. */
52+
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();
59+
}
60+
61+
start(): ContentObserver {
62+
if (this._mutationObserver) {
63+
this._mutationObserver.observe(this._element, {
64+
characterData: true,
65+
childList: true,
66+
subtree: true
67+
});
68+
}
69+
return this;
70+
}
71+
72+
pause() {
73+
if (this._mutationObserver) {
74+
this._mutationObserver.disconnect();
75+
}
76+
}
77+
78+
stop() {
79+
this.pause();
80+
this._rawChanges.complete();
81+
this._mutationObserver = null;
82+
}
83+
}
84+
85+
3886
/**
3987
* Directive that triggers a callback whenever the content of
4088
* its associated element has changed.
@@ -43,10 +91,7 @@ export class MutationObserverFactory {
4391
selector: '[cdkObserveContent]',
4492
exportAs: 'cdkObserveContent',
4593
})
46-
export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy {
47-
private _observer: MutationObserver | null;
48-
private _disabled = false;
49-
94+
export class CdkObserveContent implements AfterContentInit, OnDestroy {
5095
/** Event emitted for each change in the element's content. */
5196
@Output('cdkObserveContent') event = new EventEmitter<MutationRecord[]>();
5297

@@ -58,65 +103,42 @@ export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy
58103
get disabled() { return this._disabled; }
59104
set disabled(value: any) {
60105
this._disabled = coerceBooleanProperty(value);
106+
if (this._observer) {
107+
if (this._disabled) {
108+
this._observer.pause();
109+
} else {
110+
this._observer.start();
111+
}
112+
}
61113
}
62-
63-
/** Used for debouncing the emitted values to the observeContent event. */
64-
private _debouncer = new Subject<MutationRecord[]>();
114+
private _disabled = false;
65115

66116
/** Debounce interval for emitting the changes. */
67-
@Input() debounce: number;
117+
@Input()
118+
get debounce(): number { return this._debounce; }
119+
set debounce(value: number) {
120+
this._debounce = coerceNumberProperty(value);
121+
}
122+
private _debounce: number;
68123

69-
constructor(
70-
private _mutationObserverFactory: MutationObserverFactory,
71-
private _elementRef: ElementRef,
72-
private _ngZone: NgZone) { }
124+
private _observer: ContentObserver;
73125

74-
ngAfterContentInit() {
75-
if (this.debounce > 0) {
76-
this._ngZone.runOutsideAngular(() => {
77-
this._debouncer.pipe(debounceTime(this.debounce))
78-
.subscribe((mutations: MutationRecord[]) => this.event.emit(mutations));
79-
});
80-
} else {
81-
this._debouncer.subscribe(mutations => this.event.emit(mutations));
82-
}
126+
constructor(private _contentObserverFactory: ContentObserverFactory,
127+
private _elementRef: ElementRef, private _ngZone: NgZone) {}
83128

84-
this._observer = this._ngZone.runOutsideAngular(() => {
85-
return this._mutationObserverFactory.create((mutations: MutationRecord[]) => {
86-
this._debouncer.next(mutations);
87-
});
88-
});
129+
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)));
89134

90135
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();
136+
this._observer.start();
98137
}
99138
}
100139

101140
ngOnDestroy() {
102-
this._disable();
103-
this._debouncer.complete();
104-
}
105-
106-
private _disable() {
107-
if (this._observer) {
108-
this._observer.disconnect();
109-
}
110-
}
111-
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-
}
141+
this._observer.stop();
120142
}
121143
}
122144

0 commit comments

Comments
 (0)