Skip to content

Commit

Permalink
fix: allow using sciDimension directive in 'OnPush' change detectio…
Browse files Browse the repository at this point in the history
…n context

fixes #106

BREAKING CHANGE: Removed input property `useTimer` because no longer required as now working in the context of 'OnPush' change detection context.
  • Loading branch information
danielwiehl authored and ReToCode committed Mar 15, 2019
1 parent e2a3dcb commit cc15561
Showing 1 changed file with 62 additions and 46 deletions.
108 changes: 62 additions & 46 deletions projects/scion/dimension/src/lib/dimension.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,88 +8,104 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { Directive, DoCheck, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { fromEvent, interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Output } from '@angular/core';
import { asapScheduler, fromEvent, merge, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, startWith, takeUntil } from 'rxjs/operators';

export const NULL_DIMENSION: SciDimension = {offsetWidth: 0, offsetHeight: 0, clientWidth: 0, clientHeight: 0};

/**
* Allows observing changes to host element's size.
*
* This directive can be used in 'Default' or 'OnPush' change detection context.
*
* ---
* Usage:
* <div sciDimension (sciDimensionChange)="onDimensionChange($event)"></div>
*/
@Directive({
selector: '[sciDimension]'
selector: '[sciDimension]',
exportAs: 'sciDimension',
})
export class SciDimensionDirective implements OnInit, DoCheck, OnDestroy {
export class SciDimensionDirective implements OnDestroy {

private _host: HTMLElement;
private _dimension: SciDimension = NULL_DIMENSION;
private _destroy$ = new Subject<void>();

/**
* Emits when the dimension of the host element changes.
*/
@Output('sciDimensionChange') // tslint:disable-line:no-output-rename
public dimensionChange = new EventEmitter<SciDimension>();


/**
* By default, dimension change is detected in 'ngDoCheck' lifecycle hook.
*
* However, 'ngDoCheck' is not fired for components which are child components of components with 'OnPush' change detection strategy.
*
* If so, set this property to 'true' to detect dimension changes when a periodic timer fires.
* Controls if to emit a dimension change inside or outside of the Angular zone.
* If emitted outside of the Angular zone no change detection cycle is triggered.
*/
@Input('sciDimensionUseTimer') // tslint:disable-line:no-input-rename
public useTimer: boolean;
@Input()
public emitOutsideAngular: boolean;

constructor(host: ElementRef, private _ngZone: NgZone) {
this._host = host.nativeElement as HTMLElement;

// run outside Angular zone to not trigger app ticks on every event
// run outside of the Angular zone to not trigger app ticks on every event
this._ngZone.runOutsideAngular(() => {
fromEvent(window, 'resize')
this.dimensionChange$()
.pipe(takeUntil(this._destroy$))
.subscribe(() => this.checkDimension());
.subscribe((dimension: SciDimension) => {
if (this.emitOutsideAngular) {
this.emitDimensionChange(dimension);
}
else {
this._ngZone.run(() => this.emitDimensionChange(dimension));
}
});
});
}

public ngOnInit(): void {
this.useTimer && this._ngZone.runOutsideAngular(() => {
interval(50)
.pipe(takeUntil(this._destroy$))
.subscribe(() => this.checkDimension());
});
/**
* Returns the current dimension of its host element.
*/
public get dimension(): SciDimension {
return {
clientWidth: this._host.clientWidth,
clientHeight: this._host.clientHeight,
offsetWidth: this._host.offsetWidth,
offsetHeight: this._host.offsetHeight
};
}

public ngDoCheck(): void {
// Let Angular update the DOM first before checking for dimension change.
// Run the timer outside the Angular zone to not trigger an app tick.
this._ngZone.runOutsideAngular(() => setTimeout(this.checkDimension.bind(this)));
/**
* Emits when the dimension of the host element changes.
*
* This is a workaround until it is possible to listen for element dimension changes natively.
* @see https://wicg.github.io/ResizeObserver/
*/
private dimensionChange$(): Observable<SciDimension> {
NgZone.assertNotInAngularZone();

return merge(
this._ngZone.onStable, // When the Angular zone gets stable the dimension of the host element might have changed.
fromEvent(window, 'resize'), // However, when resizing the window, the Angular zone is not necessarily involved.
)
.pipe(
map(() => this.dimension),
startWith(NULL_DIMENSION, asapScheduler),
distinctUntilChanged((a, b) => {
return a.clientWidth === b.clientWidth &&
a.clientHeight === b.clientHeight &&
a.offsetWidth === b.offsetWidth &&
a.offsetHeight === b.offsetHeight;
}),
);
}

public ngOnDestroy(): void {
this._destroy$.next();
private emitDimensionChange(dimension: SciDimension): void {
this.dimensionChange.emit(dimension);
}

private checkDimension(): void {
const newDimension = {
offsetWidth: this._host.offsetWidth,
offsetHeight: this._host.offsetHeight,
clientWidth: this._host.clientWidth,
clientHeight: this._host.clientHeight,
};

if (this._dimension.offsetWidth === newDimension.offsetWidth
&& this._dimension.offsetHeight === newDimension.offsetHeight
&& this._dimension.clientWidth === newDimension.clientWidth
&& this._dimension.clientHeight === newDimension.clientHeight) {
return;
}

this._dimension = newDimension;
this._ngZone.run(() => this.dimensionChange.emit(this._dimension));
public ngOnDestroy(): void {
this._destroy$.next();
}
}

Expand Down

0 comments on commit cc15561

Please sign in to comment.