Meiko Meiko - 14 days ago 10
TypeScript Question

Angular2: mouse event handling (movement relative to current position)

My user should be able to move (or rotate) an object by mouse in a canvas. When mouse events occur the screen coordinates are used to calculate the delta (direction and length) to the last event. Nothing special...


  1. mousedown (get the first coordinate)

  2. mousemove (get nth coordinate, calculate deltaXY, move object by deltaXY)

  3. mouseup (same as step 2 and stop the mousemove and mouseup event handling)



After this chain of events it should be possible to repeat the same action.

This outdated example works as expected, after removing the
toRx
calls. But here the delta to the first coordinate is determined: github.com:rx-draggable

Here is my effort to adapt the code from the example:

@Component({
selector: 'home',
providers: [Scene],
template: '<canvas #canvas id="3dview"></canvas>'
})
export class Home {
@ViewChild('canvas') canvas: ElementRef;
private scene: Scene;
private mousedrag = new EventEmitter();
private mouseup = new EventEmitter<MouseEvent>();
private mousedown = new EventEmitter<MouseEvent>();
private mousemove = new EventEmitter<MouseEvent>();
private last: MouseEvent;
private el: HTMLElement;

@HostListener('mouseup', ['$event'])
onMouseup(event: MouseEvent) { this.mouseup.emit(event); }

@HostListener('mousemove', ['$event'])
onMousemove(event: MouseEvent) { this.mousemove.emit(event); }

constructor(@Inject(ElementRef) elementRef: ElementRef, scene: Scene) {
this.el = elementRef.nativeElement;
this.scene = scene;
}

@HostListener('mousedown', ['$event'])
mouseHandling(event) {
event.preventDefault();
console.log('mousedown', event);
this.last = event;
this.mousemove.subscribe({next: evt => {
console.log('mousemove.subscribe', evt);
this.mousedrag.emit(evt);
}});
this.mouseup.subscribe({next: evt => {
console.log('mousemove.subscribe', evt);
this.mousedrag.emit(evt);
this.mousemove.unsubscribe();
this.mouseup.unsubscribe();
}});
}

ngOnInit() {
console.log('init');
this.mousedrag.subscribe({
next: evt => {
console.log('mousedrag.subscribe', evt);
this.scene.rotate(
evt.clientX - this.last.clientX,
evt.clientY - this.last.clientY);
this.last = evt;
}
});
}
...
}


It works only for one cycle. After the
mouseup
event i got this error:


Uncaught EXCEPTION: Error during evaluation of "mousemove"

ORIGINAL EXCEPTION: ObjectUnsubscribedError

ERROR CONTEXT: EventEvaluationErrorContext


The cancellation of the
mousemove
subscription does not work. The error is repeating for all following mousemoves.

Do you have an idea what is wrong with my code? Is there a different elegant approach to solve this problem?

Answer

I believe your problem lays with the difference between unsubscribe() and remove(sub : Subscription) on an EventEmitter. But it is possible to do it without the use of subscriptions (except the ones created by a @HostListener) and make it easy to read. I've rewritten your code a little. You might consider though placing your mouseup event on the document or window, otherwise you get weird behaviour if you release your mouse outside the canvas.

Warning: untested code ahead

@Component({
    selector: 'home',
    providers: [Scene],
    template: '<canvas #canvas id="3dview"></canvas>'
})
export class Home {
    @ViewChild('canvas') 
    canvas: ElementRef;

    private scene: Scene;
    private last: MouseEvent;
    private el: HTMLElement;

    private mouseDown : boolean = false;

    @HostListener('mouseup')
    onMouseup() {
        this.mouseDown = false;
    }

    @HostListener('mousemove', ['$event'])
    onMousemove(event: MouseEvent) {
        if(this.mouseDown) {
           this.scene.rotate(
              event.clientX - this.last.clientX,
              event.clientY - this.last.clientY
           );
           this.last = event;
        }
    }

    @HostListener('mousedown', ['$event'])
    onMousedown(event) {
        this.mouseDown = true;
        this.last = event;
    }

    constructor(elementRef: ElementRef, scene: Scene) {
        this.el = elementRef.nativeElement;
        this.scene = scene;
    }
}
Comments