From aa2e76c650afb6919c0e983d6dd9d5676b55bb8e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 25 Jan 2018 16:37:35 +0100 Subject: [PATCH] feat(observe-content): allow for the MutationObserver to be disabled (#9025) Adds the ability for users to disable the underlying `MutationObserver` inside the `CdkObserveContent` directive. This can be useful in the cases where it might be expensive to continue observing an element while it is invisible (e.g. an item inside of a closed dropdown). --- src/cdk/observers/BUILD.bazel | 2 +- src/cdk/observers/observe-content.spec.ts | 45 ++++++++++++++++++++--- src/cdk/observers/observe-content.ts | 45 +++++++++++++++++++---- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/src/cdk/observers/BUILD.bazel b/src/cdk/observers/BUILD.bazel index ac0a6801e7e9..c8c31d89f038 100644 --- a/src/cdk/observers/BUILD.bazel +++ b/src/cdk/observers/BUILD.bazel @@ -5,6 +5,6 @@ ng_module( name = "observers", srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), module_name = "@angular/cdk/observers", - deps = ["@rxjs"], + deps = ["@rxjs", "//src/cdk/coercion"], tsconfig = ":tsconfig-build.json", ) diff --git a/src/cdk/observers/observe-content.spec.ts b/src/cdk/observers/observe-content.spec.ts index 7cb175245d80..189252618d45 100644 --- a/src/cdk/observers/observe-content.spec.ts +++ b/src/cdk/observers/observe-content.spec.ts @@ -22,11 +22,11 @@ describe('Observe content', () => { // If the hint label is empty, expect no label. const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => { - expect(spy.calls.any()).toBe(true); + expect(spy).toHaveBeenCalled(); done(); }); - expect(spy.calls.any()).toBe(false); + expect(spy).not.toHaveBeenCalled(); fixture.componentInstance.text = 'text'; fixture.detectChanges(); @@ -38,15 +38,43 @@ describe('Observe content', () => { // If the hint label is empty, expect no label. const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => { - expect(spy.calls.any()).toBe(true); + expect(spy).toHaveBeenCalled(); done(); }); - expect(spy.calls.any()).toBe(false); + expect(spy).not.toHaveBeenCalled(); fixture.componentInstance.text = 'text'; fixture.detectChanges(); }); + + it('should disconnect the MutationObserver when the directive is disabled', () => { + const observeSpy = jasmine.createSpy('observe spy'); + const disconnectSpy = jasmine.createSpy('disconnect spy'); + + // Note: since we can't know exactly when the native MutationObserver will emit, we can't + // test this scenario reliably without risking flaky tests, which is why we supply a mock + // MutationObserver and check that the methods are called at the right time. + TestBed.overrideProvider(MutationObserverFactory, { + deps: [], + useFactory: () => ({ + create: () => ({observe: observeSpy, disconnect: disconnectSpy}) + }) + }); + + const fixture = TestBed.createComponent(ComponentWithTextContent); + fixture.detectChanges(); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(disconnectSpy).not.toHaveBeenCalled(); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(disconnectSpy).toHaveBeenCalledTimes(1); + }); + }); describe('debounced', () => { @@ -93,9 +121,16 @@ describe('Observe content', () => { }); -@Component({ template: `
{{text}}
` }) +@Component({ + template: ` +
{{text}}
+ ` +}) class ComponentWithTextContent { text = ''; + disabled = false; doSomething() {} } diff --git a/src/cdk/observers/observe-content.ts b/src/cdk/observers/observe-content.ts index 3ba74c12269a..508b6ee63deb 100644 --- a/src/cdk/observers/observe-content.ts +++ b/src/cdk/observers/observe-content.ts @@ -17,7 +17,10 @@ import { AfterContentInit, Injectable, NgZone, + OnChanges, + SimpleChanges, } from '@angular/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {Subject} from 'rxjs/Subject'; import {debounceTime} from 'rxjs/operators/debounceTime'; @@ -40,12 +43,23 @@ export class MutationObserverFactory { selector: '[cdkObserveContent]', exportAs: 'cdkObserveContent', }) -export class CdkObserveContent implements AfterContentInit, OnDestroy { +export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy { private _observer: MutationObserver | null; + private _disabled = false; /** Event emitted for each change in the element's content. */ @Output('cdkObserveContent') event = new EventEmitter(); + /** + * Whether observing content is disabled. This option can be used + * to disconnect the underlying MutationObserver until it is needed. + */ + @Input('cdkObserveContentDisabled') + get disabled() { return this._disabled; } + set disabled(value: any) { + this._disabled = coerceBooleanProperty(value); + } + /** Used for debouncing the emitted values to the observeContent event. */ private _debouncer = new Subject(); @@ -73,21 +87,36 @@ export class CdkObserveContent implements AfterContentInit, OnDestroy { }); }); - if (this._observer) { - this._observer.observe(this._elementRef.nativeElement, { - 'characterData': true, - 'childList': true, - 'subtree': true - }); + if (!this.disabled) { + this._enable(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['disabled']) { + changes['disabled'].currentValue ? this._disable() : this._enable(); } } ngOnDestroy() { + this._disable(); + this._debouncer.complete(); + } + + private _disable() { if (this._observer) { this._observer.disconnect(); } + } - this._debouncer.complete(); + private _enable() { + if (this._observer) { + this._observer.observe(this._elementRef.nativeElement, { + characterData: true, + childList: true, + subtree: true + }); + } } }