diff --git a/src/lib/core/observe-content/observe-content.spec.ts b/src/lib/core/observe-content/observe-content.spec.ts index 330a812b2e7c..798987e7f164 100644 --- a/src/lib/core/observe-content/observe-content.spec.ts +++ b/src/lib/core/observe-content/observe-content.spec.ts @@ -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; + 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: `
{{text}}
` +}) +class ComponentWithDebouncedListener { + debounce = 500; + spy = jasmine.createSpy('MutationObserver callback'); +} diff --git a/src/lib/core/observe-content/observe-content.ts b/src/lib/core/observe-content/observe-content.ts index 1b2ea90b7da9..5c410be4dcf8 100644 --- a/src/lib/core/observe-content/observe-content.ts +++ b/src/lib/core/observe-content/observe-content.ts @@ -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(); + @Output('cdkObserveContent') event = new EventEmitter(); + + /** Used for debouncing the emitted values to the observeContent event. */ + private _debouncer = new Subject(); - constructor(private _elementRef: ElementRef) {} + /** 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 {} diff --git a/tools/gulp/util/rollup-helper.ts b/tools/gulp/util/rollup-helper.ts index 32f85561bb43..fe56b6932610 100644 --- a/tools/gulp/util/rollup-helper.ts +++ b/tools/gulp/util/rollup-helper.ts @@ -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' };