Skip to content

Commit 3c99d40

Browse files
author
Matt Lewis
committed
perf(draggable): lazily create all mouse event listeners
1 parent 44a4f19 commit 3c99d40

File tree

2 files changed

+81
-52
lines changed

2 files changed

+81
-52
lines changed

src/draggable.directive.ts

+76-49
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import {
22
Directive,
3-
HostListener,
43
OnInit,
54
ElementRef,
65
Renderer,
76
Output,
87
EventEmitter,
98
Input,
109
OnDestroy,
11-
NgZone
10+
OnChanges,
11+
NgZone,
12+
SimpleChanges
1213
} from '@angular/core';
1314
import {Subject} from 'rxjs/Subject';
1415
import {Observable} from 'rxjs/Observable';
@@ -35,7 +36,7 @@ const MOVE_CURSOR: string = 'move';
3536
@Directive({
3637
selector: '[mwlDraggable]'
3738
})
38-
export class Draggable implements OnInit, OnDestroy {
39+
export class Draggable implements OnInit, OnChanges, OnDestroy {
3940

4041
@Input() dropData: any;
4142

@@ -68,7 +69,13 @@ export class Draggable implements OnInit, OnDestroy {
6869
*/
6970
mouseUp: Subject<any> = new Subject();
7071

71-
private mouseMoveEventListenerUnsubscribe: Function;
72+
private eventListenerSubscriptions: {
73+
mousemove?: Function,
74+
mousedown?: Function,
75+
mouseup?: Function,
76+
mouseenter?: Function,
77+
mouseleave?: Function
78+
} = {};
7279

7380
/**
7481
* @hidden
@@ -82,6 +89,8 @@ export class Draggable implements OnInit, OnDestroy {
8289

8390
ngOnInit(): void {
8491

92+
this.checkEventListeners();
93+
8594
const mouseDrag: Observable<any> = this.mouseDown
8695
.filter(() => this.canDrag())
8796
.flatMap((mouseDownEvent: MouseEvent) => {
@@ -182,64 +191,75 @@ export class Draggable implements OnInit, OnDestroy {
182191

183192
}
184193

185-
ngOnDestroy(): void {
186-
if (this.mouseMoveEventListenerUnsubscribe) {
187-
this.mouseMoveEventListenerUnsubscribe();
194+
ngOnChanges(changes: SimpleChanges): void {
195+
if (changes['dragAxis']) {
196+
this.checkEventListeners();
188197
}
198+
}
199+
200+
ngOnDestroy(): void {
201+
this.unsubscribeEventListeners();
189202
this.mouseDown.complete();
190203
this.mouseMove.complete();
191204
this.mouseUp.complete();
192205
}
193206

194-
/**
195-
* @hidden
196-
*/
197-
@HostListener('mousedown', ['$event'])
198-
onMouseDown(event: MouseEvent): void {
199-
this.zone.runOutsideAngular(() => {
200-
if (!this.mouseMoveEventListenerUnsubscribe) {
201-
this.mouseMoveEventListenerUnsubscribe = this.renderer.listenGlobal('document', 'mousemove', (event: MouseEvent) => {
202-
this.mouseMove.next(event);
207+
private checkEventListeners(): void {
208+
209+
const canDrag: boolean = this.canDrag();
210+
const hasEventListeners: boolean = Object.keys(this.eventListenerSubscriptions).length > 0;
211+
212+
if (canDrag && !hasEventListeners) {
213+
214+
this.zone.runOutsideAngular(() => {
215+
216+
this.eventListenerSubscriptions.mousedown = this.renderer.listen(this.element.nativeElement, 'mousedown', (event: MouseEvent) => {
217+
this.onMouseDown(event);
203218
});
204-
}
205-
this.mouseDown.next(event);
206-
});
219+
220+
this.eventListenerSubscriptions.mouseup = this.renderer.listenGlobal('document', 'mouseup', (event: MouseEvent) => {
221+
this.onMouseUp(event);
222+
});
223+
224+
this.eventListenerSubscriptions.mouseenter = this.renderer.listen(this.element.nativeElement, 'mouseenter', () => {
225+
this.onMouseEnter();
226+
});
227+
228+
this.eventListenerSubscriptions.mouseleave = this.renderer.listen(this.element.nativeElement, 'mouseleave', () => {
229+
this.onMouseLeave();
230+
});
231+
232+
});
233+
234+
} else if (!canDrag && hasEventListeners) {
235+
this.unsubscribeEventListeners();
236+
}
237+
207238
}
208239

209-
/**
210-
* @hidden
211-
*/
212-
@HostListener('document:mouseup', ['$event'])
213-
onMouseUp(event: MouseEvent): void {
214-
this.zone.runOutsideAngular(() => {
215-
if (this.mouseMoveEventListenerUnsubscribe) {
216-
this.mouseMoveEventListenerUnsubscribe();
217-
this.mouseMoveEventListenerUnsubscribe = null;
218-
}
219-
this.mouseUp.next(event);
220-
});
240+
private onMouseDown(event: MouseEvent): void {
241+
if (!this.eventListenerSubscriptions.mousemove) {
242+
this.eventListenerSubscriptions.mousemove = this.renderer.listenGlobal('document', 'mousemove', (event: MouseEvent) => {
243+
this.mouseMove.next(event);
244+
});
245+
}
246+
this.mouseDown.next(event);
221247
}
222248

223-
/**
224-
* @hidden
225-
*/
226-
@HostListener('mouseenter')
227-
onMouseEnter(): void {
228-
this.zone.runOutsideAngular(() => {
229-
if (this.canDrag()) {
230-
this.setCursor(MOVE_CURSOR);
231-
}
232-
});
249+
private onMouseUp(event: MouseEvent): void {
250+
if (this.eventListenerSubscriptions.mousemove) {
251+
this.eventListenerSubscriptions.mousemove();
252+
delete this.eventListenerSubscriptions.mousemove;
253+
}
254+
this.mouseUp.next(event);
233255
}
234256

235-
/**
236-
* @hidden
237-
*/
238-
@HostListener('mouseleave')
239-
onMouseLeave(): void {
240-
this.zone.runOutsideAngular(() => {
241-
this.setCursor(null);
242-
});
257+
private onMouseEnter(): void {
258+
this.setCursor(MOVE_CURSOR);
259+
}
260+
261+
private onMouseLeave(): void {
262+
this.setCursor(null);
243263
}
244264

245265
private setCssTransform(value: string): void {
@@ -260,4 +280,11 @@ export class Draggable implements OnInit, OnDestroy {
260280
this.renderer.setElementStyle(this.element.nativeElement, 'cursor', value);
261281
}
262282

283+
private unsubscribeEventListeners(): void {
284+
Object.keys(this.eventListenerSubscriptions).forEach((type: string) => {
285+
this.eventListenerSubscriptions[type]();
286+
delete this.eventListenerSubscriptions[type];
287+
});
288+
}
289+
263290
}

test/draggable.directive.spec.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ describe('draggable directive', () => {
113113
});
114114

115115
it('should disable dragging', () => {
116+
expect(Object.keys(fixture.componentInstance.draggable['eventListenerSubscriptions']).length).to.equal(4);
116117
fixture.componentInstance.dragAxis = {x: false, y: false};
117118
fixture.detectChanges();
119+
expect(Object.keys(fixture.componentInstance.draggable['eventListenerSubscriptions']).length).to.equal(0);
118120
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
119121
triggerDomEvent('mousedown', draggableElement, {clientX: 5, clientY: 10});
120122
triggerDomEvent('mousemove', draggableElement, {clientX: 7, clientY: 12});
@@ -246,15 +248,15 @@ describe('draggable directive', () => {
246248
it('should only unregister the mouse move listener if it exists', () => {
247249
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
248250
triggerDomEvent('mouseup', draggableElement, {clientX: 7, clientY: 8});
249-
expect(fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe']).not.to.be.ok;
251+
expect(fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove).not.to.be.ok;
250252
});
251253

252254
it('should not register multiple mouse move listeners', () => {
253255
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
254256
triggerDomEvent('mousedown', draggableElement, {clientX: 7, clientY: 8});
255-
const mouseMoveUnsubscribe: Function = fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe'];
257+
const mouseMoveUnsubscribe: Function = fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove;
256258
triggerDomEvent('mousedown', draggableElement, {clientX: 7, clientY: 8});
257-
expect(fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe']).to.equal(mouseMoveUnsubscribe);
259+
expect(fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove).to.equal(mouseMoveUnsubscribe);
258260
});
259261

260262
});

0 commit comments

Comments
 (0)