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
This commit is contained in:
Kristiyan Kostadinov
2017-05-03 01:39:14 +02:00
committed by Andrew Seguin
parent b011b45ac8
commit 244aece0fe
3 changed files with 105 additions and 19 deletions
@@ -1,22 +1,22 @@
import {Component} from '@angular/core';
import {async, TestBed} from '@angular/core/testing';
import {ObserveContentModule} from './observe-content';
import {async, TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
import {ObserveContentModule, MdMutationObserverFactory} from './observe-content';
// TODO(elad): `ProxyZone` doesn't seem to capture the events raised by
// `MutationObserver` and needs to be investigated
describe('Observe content', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ObserveContentModule],
declarations: [ComponentWithTextContent, ComponentWithChildTextContent],
});
describe('basic usage', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ObserveContentModule],
declarations: [ComponentWithTextContent, ComponentWithChildTextContent]
});
TestBed.compileComponents();
}));
TestBed.compileComponents();
}));
describe('text content change', () => {
it('should call the registered for changes function', done => {
it('should trigger the callback when the content of the element changes', done => {
let fixture = TestBed.createComponent(ComponentWithTextContent);
fixture.detectChanges();
@@ -31,10 +31,8 @@ describe('Observe content', () => {
fixture.componentInstance.text = 'text';
fixture.detectChanges();
});
});
describe('child text content change', () => {
it('should call the registered for changes function', done => {
it('should trigger the callback when the content of the children changes', done => {
let fixture = TestBed.createComponent(ComponentWithChildTextContent);
fixture.detectChanges();
@@ -50,6 +48,48 @@ describe('Observe content', () => {
fixture.detectChanges();
});
});
describe('debounced', () => {
let fixture: ComponentFixture<ComponentWithDebouncedListener>;
let callbacks: Function[];
let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));
beforeEach(async(() => {
callbacks = [];
TestBed.configureTestingModule({
imports: [ObserveContentModule],
declarations: [ComponentWithDebouncedListener],
providers: [{
provide: MdMutationObserverFactory,
useValue: {
create: function(callback: Function) {
callbacks.push(callback);
return {
observe: () => {},
disconnect: () => {}
};
}
}
}]
});
TestBed.compileComponents();
fixture = TestBed.createComponent(ComponentWithDebouncedListener);
fixture.detectChanges();
}));
it('should debounce the content changes', fakeAsync(() => {
invokeCallbacks();
invokeCallbacks();
invokeCallbacks();
tick(500);
expect(fixture.componentInstance.spy).toHaveBeenCalledTimes(1);
}));
});
});
@@ -64,3 +104,11 @@ class ComponentWithChildTextContent {
text = '';
doSomething() {}
}
@Component({
template: `<div (cdkObserveContent)="spy($event)" [debounce]="debounce">{{text}}</div>`
})
class ComponentWithDebouncedListener {
debounce = 500;
spy = jasmine.createSpy('MutationObserver callback');
}
@@ -3,10 +3,25 @@ import {
ElementRef,
NgModule,
Output,
Input,
EventEmitter,
OnDestroy,
AfterContentInit
AfterContentInit,
Injectable,
} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/debounceTime';
/**
* Factory that creates a new MutationObserver and allows us to stub it out in unit tests.
* @docs-private
*/
@Injectable()
export class MdMutationObserverFactory {
create(callback): MutationObserver {
return new MutationObserver(callback);
}
}
/**
* Directive that triggers a callback whenever the content of
@@ -19,12 +34,30 @@ export class ObserveContent implements AfterContentInit, OnDestroy {
private _observer: MutationObserver;
/** Event emitted for each change in the element's content. */
@Output('cdkObserveContent') event = new EventEmitter<void>();
@Output('cdkObserveContent') event = new EventEmitter<MutationRecord[]>();
constructor(private _elementRef: ElementRef) {}
/** Used for debouncing the emitted values to the observeContent event. */
private _debouncer = new Subject<MutationRecord[]>();
/** Debounce interval for emitting the changes. */
@Input() debounce: number;
constructor(
private _mutationObserverFactory: MdMutationObserverFactory,
private _elementRef: ElementRef) { }
ngAfterContentInit() {
this._observer = new MutationObserver(mutations => mutations.forEach(() => this.event.emit()));
if (this.debounce > 0) {
this._debouncer
.debounceTime(this.debounce)
.subscribe(mutations => this.event.emit(mutations));
} else {
this._debouncer.subscribe(mutations => this.event.emit(mutations));
}
this._observer = this._mutationObserverFactory.create((mutations: MutationRecord[]) => {
this._debouncer.next(mutations);
});
this._observer.observe(this._elementRef.nativeElement, {
characterData: true,
@@ -36,12 +69,16 @@ export class ObserveContent implements AfterContentInit, OnDestroy {
ngOnDestroy() {
if (this._observer) {
this._observer.disconnect();
this._debouncer.complete();
this._debouncer = this._observer = null;
}
}
}
@NgModule({
exports: [ObserveContent],
declarations: [ObserveContent]
declarations: [ObserveContent],
providers: [MdMutationObserverFactory]
})
export class ObserveContentModule {}
+1
View File
@@ -36,6 +36,7 @@ const ROLLUP_GLOBALS = {
'rxjs/add/operator/first': 'Rx.Observable.prototype',
'rxjs/add/operator/startWith': 'Rx.Observable.prototype',
'rxjs/add/operator/switchMap': 'Rx.Observable.prototype',
'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype',
'rxjs/Observable': 'Rx'
};