Skip to content

Commit 078b4ba

Browse files
authored
Merge pull request #12 from sergkr/change-detection-fix
Avoid triggering change detection unless necessary
2 parents 1b8dd7c + e90067b commit 078b4ba

File tree

6 files changed

+75
-25
lines changed

6 files changed

+75
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ src/**/*.js
1212
e2e/**/*.js
1313
e2e/**/*.js.map
1414
coverage
15+
.idea/

src/lib/src/in-viewport/in-viewport.directive.spec.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
import { fakeAsync, tick } from '@angular/core/testing';
2-
import { ElementRef } from '@angular/core';
2+
import { ElementRef, NgZone } from '@angular/core';
33
import { WindowRef } from '../window/window.service';
44
import { InViewportDirective } from './in-viewport.directive';
5+
import { FakeDOMStandardElement } from '../testing/dom';
6+
import { MockNgZone } from '../testing/mock-ng-zone';
57

68
describe('InViewportDirective', () => {
79
let node: HTMLElement;
810
let el: ElementRef;
911
let directive: InViewportDirective;
10-
const renderer = {
11-
listen: (element: HTMLElement, event: string, callback: () => void) => { }
12-
};
1312
const text = 'Exercitation est eu reprehenderit veniam anim veniam enim laboris nisi.';
1413
let windowRef: WindowRef;
1514
let rectSpy = jasmine.createSpy('rectSpy');
1615
let cdRef = { detectChanges: () => {} };
16+
let zone: NgZone;
1717

1818
beforeEach(() => {
19-
windowRef = new WindowRef();
19+
windowRef = new FakeDOMStandardElement('window') as any as WindowRef;
2020
windowRef.innerWidth = 1366;
2121
windowRef.innerHeight = 768;
22+
(<any>windowRef).addEventListener = () => {};
2223
rectSpy.and.returnValue({ left: 0, right: 1366, top: 0, bottom: 500 });
2324
node = document.createElement('p');
2425
node.innerText = text;
2526
el = new ElementRef(node);
2627
el.nativeElement.getBoundingClientRect = rectSpy;
27-
directive = new InViewportDirective(el, <any>renderer, windowRef, <any>cdRef);
28+
zone = new MockNgZone();
29+
directive = new InViewportDirective(el, windowRef, <any>cdRef, zone);
2830
directive.ngAfterViewInit();
2931
});
3032

@@ -144,18 +146,12 @@ describe('InViewportDirective', () => {
144146
describe('scrollable parent element', () => {
145147
it('should add event handler for parent element scroll events', () => {
146148
const div = document.createElement('div');
147-
const spy = spyOn(renderer, 'listen');
149+
const spy = spyOn(div, 'addEventListener');
148150
directive.parentEl = div;
149151
directive.ngAfterViewInit();
150152
expect(spy).toHaveBeenCalled();
151153
});
152154

153-
it('should do nothing if no parent element', () => {
154-
const spy = spyOn(renderer, 'listen');
155-
directive.ngAfterViewInit();
156-
expect(spy).not.toHaveBeenCalled();
157-
});
158-
159155
it('should emit next value in viewport$ observable', fakeAsync(() => {
160156
const spy = spyOn(directive, 'calculateInViewportStatus');
161157
directive.onParentScroll();

src/lib/src/in-viewport/in-viewport.directive.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import {
2-
Directive, ElementRef, HostListener, HostBinding,
3-
EventEmitter, Input, Output, OnDestroy, AfterViewInit,
4-
Renderer2, ChangeDetectorRef
2+
Directive, ElementRef, HostBinding, EventEmitter,
3+
Input, Output, OnDestroy, AfterViewInit,
4+
ChangeDetectorRef, NgZone
55
} from '@angular/core';
6+
import { Observable } from 'rxjs/Observable';
67
import { Subject } from 'rxjs/Subject';
8+
import { fromEvent } from 'rxjs/observable/fromEvent';
9+
import 'rxjs/add/operator/auditTime';
710
import 'rxjs/add/operator/debounceTime';
811
import 'rxjs/add/operator/takeUntil';
12+
import 'rxjs/add/observable/merge';
913

1014
import { WindowRef } from '../window/window.service';
1115
import { Viewport } from '../shared/viewport.model';
@@ -123,13 +127,16 @@ export class InViewportDirective implements AfterViewInit, OnDestroy {
123127
/**
124128
* Creates an instance of InViewportDirective.
125129
* @param {ElementRef} el
130+
* @param {WindowRef} win
131+
* @param {ChangeDetectorRef} cdRef
132+
* @param {NgZone} ngZone
126133
* @memberof InViewportDirective
127134
*/
128135
constructor(
129136
private el: ElementRef,
130-
private renderer: Renderer2,
131137
private win: WindowRef,
132-
private cdRef: ChangeDetectorRef
138+
private cdRef: ChangeDetectorRef,
139+
private ngZone: NgZone
133140
) { }
134141
/**
135142
* Subscribe to `viewport$` observable which
@@ -146,8 +153,22 @@ export class InViewportDirective implements AfterViewInit, OnDestroy {
146153
.debounceTime(this.debounce)
147154
.subscribe(() => this.calculateInViewportStatus());
148155

156+
// Listen for window scroll/resize events.
157+
this.ngZone.runOutsideAngular(() => {
158+
Observable.merge(
159+
fromEvent(this.win as any, eventData.eventWindowResize),
160+
fromEvent(this.win as any, eventData.eventWindowScroll)
161+
)
162+
.auditTime(this.debounce)
163+
.subscribe(() => this.onViewportChange());
164+
});
165+
149166
if (this.parentEl) {
150-
this.renderer.listen(this.parentEl, eventData.eventScroll, this.onParentScroll.bind(this));
167+
this.ngZone.runOutsideAngular(() => {
168+
fromEvent(this.parentEl, eventData.eventScroll)
169+
.auditTime(this.debounce)
170+
.subscribe(() => this.onParentScroll());
171+
});
151172
}
152173
}
153174
/**
@@ -165,16 +186,13 @@ export class InViewportDirective implements AfterViewInit, OnDestroy {
165186
*
166187
* @memberof InViewportDirective
167188
*/
168-
@HostListener(eventData.eventWindowScroll)
169-
@HostListener(eventData.eventWindowResize)
170189
public onViewportChange(): void {
171190
this.viewport$.next();
172191
}
173192
/**
174193
* Calculate inViewport status and emit event
175194
* when viewport status has changed
176195
*
177-
* @param {Viewport} viewport
178196
* @memberof InViewportDirective
179197
*/
180198
public calculateInViewportStatus(): void {
@@ -194,7 +212,7 @@ export class InViewportDirective implements AfterViewInit, OnDestroy {
194212
this.inViewport = (inParentViewport && inWindowViewport);
195213

196214
if (oldInViewport !== this.inViewport) {
197-
this.onInViewportChange.emit(this.inViewport);
215+
this.ngZone.run(() => this.onInViewportChange.emit(this.inViewport));
198216
}
199217
}
200218
/**

src/lib/src/shared/event-data.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const eventPathResize = [
1010
'$event.target.scrollY',
1111
'$event.target.scrollX'
1212
];
13-
export const eventWindowResize = 'window:resize';
14-
export const eventWindowScroll = 'window:scroll';
13+
export const eventWindowResize = 'resize';
14+
export const eventWindowScroll = 'scroll';
1515
export const inViewportClass = 'class.sn-viewport-in';
1616
export const notInViewportClass = 'class.sn-viewport-out';
1717

src/lib/src/testing/dom.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export class FakeDOMStandardElement {
2+
private listeners: object;
3+
private nodeName: string;
4+
5+
constructor(nodeName: string) {
6+
this.listeners = {};
7+
this.nodeName = nodeName;
8+
}
9+
10+
addEventListener(eventName: string, handler: any, useCapture: boolean) {
11+
this.listeners[eventName] = handler;
12+
}
13+
14+
removeEventListener(eventName: string, handler: any, useCapture: boolean) {
15+
delete this.listeners[eventName];
16+
}
17+
18+
trigger(eventName: string) {
19+
let args = Array.prototype.slice.call(arguments, 1);
20+
if (eventName in this.listeners) {
21+
this.listeners[eventName].apply(null, args);
22+
}
23+
}
24+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { EventEmitter, NgZone } from '@angular/core';
2+
3+
export class MockNgZone extends NgZone {
4+
private _mockOnStable: EventEmitter<any> = new EventEmitter(false);
5+
constructor() { super({enableLongStackTrace: false}); }
6+
set onStable(v) {}
7+
get onStable() { return this._mockOnStable; }
8+
run(fn: Function): any { return fn(); }
9+
runOutsideAngular(fn: Function): any { return fn(); }
10+
simulateZoneExit(): void { this.onStable.emit(null); }
11+
}

0 commit comments

Comments
 (0)