Skip to content

Commit

Permalink
feat(observe-content): allow for the MutationObserver to be disabled (#…
Browse files Browse the repository at this point in the history
…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).
  • Loading branch information
crisbeto authored and jelbourn committed Jan 25, 2018
1 parent 53c45ec commit aa2e76c
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/cdk/observers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
45 changes: 40 additions & 5 deletions src/cdk/observers/observe-content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -93,9 +121,16 @@ describe('Observe content', () => {
});


@Component({ template: `<div (cdkObserveContent)="doSomething()">{{text}}</div>` })
@Component({
template: `
<div
(cdkObserveContent)="doSomething()"
[cdkObserveContentDisabled]="disabled">{{text}}</div>
`
})
class ComponentWithTextContent {
text = '';
disabled = false;
doSomething() {}
}

Expand Down
45 changes: 37 additions & 8 deletions src/cdk/observers/observe-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<MutationRecord[]>();

/**
* 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<MutationRecord[]>();

Expand Down Expand Up @@ -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
});
}
}
}

Expand Down

0 comments on commit aa2e76c

Please sign in to comment.