Skip to content

Commit 244aece

Browse files
crisbetoandrewseguin
authored andcommitted
feat(observe-content): add debounce option and other improvements (#2404)
* feat(observe-content): add debounce option and other improvements * Adds a reusable utility for debouncing a function. * Adds the ability to debounce the changes from the `cdkObserveContent` directive. * Makes the `cdkObserveContent` directive pass back the `MutationRecord` to the `EventEmitter`. * Fires the callback once per mutation event, instead of once per `MutationRecord`. Relates to #2372. * Remove flaky tests. * Stub out the MutationObserver. * refactor: use debounce logic from rxjs * chore: fix lint error * fix: refactor the MutationObserver token to not break AoT
1 parent b011b45 commit 244aece

File tree

3 files changed

+105
-19
lines changed

3 files changed

+105
-19
lines changed

src/lib/core/observe-content/observe-content.spec.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import {Component} from '@angular/core';
2-
import {async, TestBed} from '@angular/core/testing';
3-
import {ObserveContentModule} from './observe-content';
2+
import {async, TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
3+
import {ObserveContentModule, MdMutationObserverFactory} 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

88
describe('Observe content', () => {
9-
beforeEach(async(() => {
10-
TestBed.configureTestingModule({
11-
imports: [ObserveContentModule],
12-
declarations: [ComponentWithTextContent, ComponentWithChildTextContent],
13-
});
9+
describe('basic usage', () => {
10+
beforeEach(async(() => {
11+
TestBed.configureTestingModule({
12+
imports: [ObserveContentModule],
13+
declarations: [ComponentWithTextContent, ComponentWithChildTextContent]
14+
});
1415

15-
TestBed.compileComponents();
16-
}));
16+
TestBed.compileComponents();
17+
}));
1718

18-
describe('text content change', () => {
19-
it('should call the registered for changes function', done => {
19+
it('should trigger the callback when the content of the element changes', done => {
2020
let fixture = TestBed.createComponent(ComponentWithTextContent);
2121
fixture.detectChanges();
2222

@@ -31,10 +31,8 @@ describe('Observe content', () => {
3131
fixture.componentInstance.text = 'text';
3232
fixture.detectChanges();
3333
});
34-
});
3534

36-
describe('child text content change', () => {
37-
it('should call the registered for changes function', done => {
35+
it('should trigger the callback when the content of the children changes', done => {
3836
let fixture = TestBed.createComponent(ComponentWithChildTextContent);
3937
fixture.detectChanges();
4038

@@ -50,6 +48,48 @@ describe('Observe content', () => {
5048
fixture.detectChanges();
5149
});
5250
});
51+
52+
describe('debounced', () => {
53+
let fixture: ComponentFixture<ComponentWithDebouncedListener>;
54+
let callbacks: Function[];
55+
let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));
56+
57+
beforeEach(async(() => {
58+
callbacks = [];
59+
60+
TestBed.configureTestingModule({
61+
imports: [ObserveContentModule],
62+
declarations: [ComponentWithDebouncedListener],
63+
providers: [{
64+
provide: MdMutationObserverFactory,
65+
useValue: {
66+
create: function(callback: Function) {
67+
callbacks.push(callback);
68+
69+
return {
70+
observe: () => {},
71+
disconnect: () => {}
72+
};
73+
}
74+
}
75+
}]
76+
});
77+
78+
TestBed.compileComponents();
79+
80+
fixture = TestBed.createComponent(ComponentWithDebouncedListener);
81+
fixture.detectChanges();
82+
}));
83+
84+
it('should debounce the content changes', fakeAsync(() => {
85+
invokeCallbacks();
86+
invokeCallbacks();
87+
invokeCallbacks();
88+
89+
tick(500);
90+
expect(fixture.componentInstance.spy).toHaveBeenCalledTimes(1);
91+
}));
92+
});
5393
});
5494

5595

@@ -64,3 +104,11 @@ class ComponentWithChildTextContent {
64104
text = '';
65105
doSomething() {}
66106
}
107+
108+
@Component({
109+
template: `<div (cdkObserveContent)="spy($event)" [debounce]="debounce">{{text}}</div>`
110+
})
111+
class ComponentWithDebouncedListener {
112+
debounce = 500;
113+
spy = jasmine.createSpy('MutationObserver callback');
114+
}

src/lib/core/observe-content/observe-content.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@ import {
33
ElementRef,
44
NgModule,
55
Output,
6+
Input,
67
EventEmitter,
78
OnDestroy,
8-
AfterContentInit
9+
AfterContentInit,
10+
Injectable,
911
} from '@angular/core';
12+
import {Subject} from 'rxjs/Subject';
13+
import 'rxjs/add/operator/debounceTime';
14+
15+
/**
16+
* Factory that creates a new MutationObserver and allows us to stub it out in unit tests.
17+
* @docs-private
18+
*/
19+
@Injectable()
20+
export class MdMutationObserverFactory {
21+
create(callback): MutationObserver {
22+
return new MutationObserver(callback);
23+
}
24+
}
1025

1126
/**
1227
* Directive that triggers a callback whenever the content of
@@ -19,12 +34,30 @@ export class ObserveContent implements AfterContentInit, OnDestroy {
1934
private _observer: MutationObserver;
2035

2136
/** Event emitted for each change in the element's content. */
22-
@Output('cdkObserveContent') event = new EventEmitter<void>();
37+
@Output('cdkObserveContent') event = new EventEmitter<MutationRecord[]>();
38+
39+
/** Used for debouncing the emitted values to the observeContent event. */
40+
private _debouncer = new Subject<MutationRecord[]>();
2341

24-
constructor(private _elementRef: ElementRef) {}
42+
/** Debounce interval for emitting the changes. */
43+
@Input() debounce: number;
44+
45+
constructor(
46+
private _mutationObserverFactory: MdMutationObserverFactory,
47+
private _elementRef: ElementRef) { }
2548

2649
ngAfterContentInit() {
27-
this._observer = new MutationObserver(mutations => mutations.forEach(() => this.event.emit()));
50+
if (this.debounce > 0) {
51+
this._debouncer
52+
.debounceTime(this.debounce)
53+
.subscribe(mutations => this.event.emit(mutations));
54+
} else {
55+
this._debouncer.subscribe(mutations => this.event.emit(mutations));
56+
}
57+
58+
this._observer = this._mutationObserverFactory.create((mutations: MutationRecord[]) => {
59+
this._debouncer.next(mutations);
60+
});
2861

2962
this._observer.observe(this._elementRef.nativeElement, {
3063
characterData: true,
@@ -36,12 +69,16 @@ export class ObserveContent implements AfterContentInit, OnDestroy {
3669
ngOnDestroy() {
3770
if (this._observer) {
3871
this._observer.disconnect();
72+
this._debouncer.complete();
73+
this._debouncer = this._observer = null;
3974
}
4075
}
4176
}
4277

78+
4379
@NgModule({
4480
exports: [ObserveContent],
45-
declarations: [ObserveContent]
81+
declarations: [ObserveContent],
82+
providers: [MdMutationObserverFactory]
4683
})
4784
export class ObserveContentModule {}

tools/gulp/util/rollup-helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const ROLLUP_GLOBALS = {
3636
'rxjs/add/operator/first': 'Rx.Observable.prototype',
3737
'rxjs/add/operator/startWith': 'Rx.Observable.prototype',
3838
'rxjs/add/operator/switchMap': 'Rx.Observable.prototype',
39+
'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype',
3940
'rxjs/Observable': 'Rx'
4041
};
4142

0 commit comments

Comments
 (0)