mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Andrew Seguin
parent
b011b45ac8
commit
244aece0fe
@@ -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 {}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user