Skip to content

Commit

Permalink
perf(draggable): lazily create all mouse event listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Lewis committed Mar 4, 2017
1 parent 44a4f19 commit 3c99d40
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 52 deletions.
125 changes: 76 additions & 49 deletions src/draggable.directive.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
Directive,
HostListener,
OnInit,
ElementRef,
Renderer,
Output,
EventEmitter,
Input,
OnDestroy,
NgZone
OnChanges,
NgZone,
SimpleChanges
} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
Expand All @@ -35,7 +36,7 @@ const MOVE_CURSOR: string = 'move';
@Directive({
selector: '[mwlDraggable]'
})
export class Draggable implements OnInit, OnDestroy {
export class Draggable implements OnInit, OnChanges, OnDestroy {

@Input() dropData: any;

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

private mouseMoveEventListenerUnsubscribe: Function;
private eventListenerSubscriptions: {
mousemove?: Function,
mousedown?: Function,
mouseup?: Function,
mouseenter?: Function,
mouseleave?: Function
} = {};

/**
* @hidden
Expand All @@ -82,6 +89,8 @@ export class Draggable implements OnInit, OnDestroy {

ngOnInit(): void {

this.checkEventListeners();

const mouseDrag: Observable<any> = this.mouseDown
.filter(() => this.canDrag())
.flatMap((mouseDownEvent: MouseEvent) => {
Expand Down Expand Up @@ -182,64 +191,75 @@ export class Draggable implements OnInit, OnDestroy {

}

ngOnDestroy(): void {
if (this.mouseMoveEventListenerUnsubscribe) {
this.mouseMoveEventListenerUnsubscribe();
ngOnChanges(changes: SimpleChanges): void {
if (changes['dragAxis']) {
this.checkEventListeners();
}
}

ngOnDestroy(): void {
this.unsubscribeEventListeners();
this.mouseDown.complete();
this.mouseMove.complete();
this.mouseUp.complete();
}

/**
* @hidden
*/
@HostListener('mousedown', ['$event'])
onMouseDown(event: MouseEvent): void {
this.zone.runOutsideAngular(() => {
if (!this.mouseMoveEventListenerUnsubscribe) {
this.mouseMoveEventListenerUnsubscribe = this.renderer.listenGlobal('document', 'mousemove', (event: MouseEvent) => {
this.mouseMove.next(event);
private checkEventListeners(): void {

const canDrag: boolean = this.canDrag();
const hasEventListeners: boolean = Object.keys(this.eventListenerSubscriptions).length > 0;

if (canDrag && !hasEventListeners) {

this.zone.runOutsideAngular(() => {

this.eventListenerSubscriptions.mousedown = this.renderer.listen(this.element.nativeElement, 'mousedown', (event: MouseEvent) => {
this.onMouseDown(event);
});
}
this.mouseDown.next(event);
});

this.eventListenerSubscriptions.mouseup = this.renderer.listenGlobal('document', 'mouseup', (event: MouseEvent) => {
this.onMouseUp(event);
});

this.eventListenerSubscriptions.mouseenter = this.renderer.listen(this.element.nativeElement, 'mouseenter', () => {
this.onMouseEnter();
});

this.eventListenerSubscriptions.mouseleave = this.renderer.listen(this.element.nativeElement, 'mouseleave', () => {
this.onMouseLeave();
});

});

} else if (!canDrag && hasEventListeners) {
this.unsubscribeEventListeners();
}

}

/**
* @hidden
*/
@HostListener('document:mouseup', ['$event'])
onMouseUp(event: MouseEvent): void {
this.zone.runOutsideAngular(() => {
if (this.mouseMoveEventListenerUnsubscribe) {
this.mouseMoveEventListenerUnsubscribe();
this.mouseMoveEventListenerUnsubscribe = null;
}
this.mouseUp.next(event);
});
private onMouseDown(event: MouseEvent): void {
if (!this.eventListenerSubscriptions.mousemove) {
this.eventListenerSubscriptions.mousemove = this.renderer.listenGlobal('document', 'mousemove', (event: MouseEvent) => {
this.mouseMove.next(event);
});
}
this.mouseDown.next(event);
}

/**
* @hidden
*/
@HostListener('mouseenter')
onMouseEnter(): void {
this.zone.runOutsideAngular(() => {
if (this.canDrag()) {
this.setCursor(MOVE_CURSOR);
}
});
private onMouseUp(event: MouseEvent): void {
if (this.eventListenerSubscriptions.mousemove) {
this.eventListenerSubscriptions.mousemove();
delete this.eventListenerSubscriptions.mousemove;
}
this.mouseUp.next(event);
}

/**
* @hidden
*/
@HostListener('mouseleave')
onMouseLeave(): void {
this.zone.runOutsideAngular(() => {
this.setCursor(null);
});
private onMouseEnter(): void {
this.setCursor(MOVE_CURSOR);
}

private onMouseLeave(): void {
this.setCursor(null);
}

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

private unsubscribeEventListeners(): void {
Object.keys(this.eventListenerSubscriptions).forEach((type: string) => {
this.eventListenerSubscriptions[type]();
delete this.eventListenerSubscriptions[type];
});
}

}
8 changes: 5 additions & 3 deletions test/draggable.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ describe('draggable directive', () => {
});

it('should disable dragging', () => {
expect(Object.keys(fixture.componentInstance.draggable['eventListenerSubscriptions']).length).to.equal(4);
fixture.componentInstance.dragAxis = {x: false, y: false};
fixture.detectChanges();
expect(Object.keys(fixture.componentInstance.draggable['eventListenerSubscriptions']).length).to.equal(0);
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
triggerDomEvent('mousedown', draggableElement, {clientX: 5, clientY: 10});
triggerDomEvent('mousemove', draggableElement, {clientX: 7, clientY: 12});
Expand Down Expand Up @@ -246,15 +248,15 @@ describe('draggable directive', () => {
it('should only unregister the mouse move listener if it exists', () => {
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
triggerDomEvent('mouseup', draggableElement, {clientX: 7, clientY: 8});
expect(fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe']).not.to.be.ok;
expect(fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove).not.to.be.ok;
});

it('should not register multiple mouse move listeners', () => {
const draggableElement: HTMLElement = fixture.componentInstance.draggable.element.nativeElement;
triggerDomEvent('mousedown', draggableElement, {clientX: 7, clientY: 8});
const mouseMoveUnsubscribe: Function = fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe'];
const mouseMoveUnsubscribe: Function = fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove;
triggerDomEvent('mousedown', draggableElement, {clientX: 7, clientY: 8});
expect(fixture.componentInstance.draggable['mouseMoveEventListenerUnsubscribe']).to.equal(mouseMoveUnsubscribe);
expect(fixture.componentInstance.draggable['eventListenerSubscriptions'].mousemove).to.equal(mouseMoveUnsubscribe);
});

});

0 comments on commit 3c99d40

Please sign in to comment.