Handling mouse events with Angular 2 (tracking movement based on current position)

One of the features I want to implement for my user is the ability to move or rotate an object in a canvas using the mouse. The process involves calculating the delta (direction and length) between successive mouse events in order to update the position of the object on the screen.

  1. When the user presses down on the mouse button, the initial coordinates are captured.
  2. As the user moves the mouse, subsequent coordinates are recorded and used to calculate the deltaXY, which is then used to adjust the position of the object accordingly.
  3. Upon releasing the mouse button, the last set of coordinates is used to finalize the movement and stop handling further mouse events.

Once this sequence of events is completed, the action should be repeatable.

This example seems outdated but functional once the unnecessary toRx calls are removed. You can find the code at: github.com:rx-draggable

I have attempted to adapt the code from the provided 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;
      }
    });
  }
  ...
}

Although the functionality works for one cycle, upon triggering the mouseup event, I encountered the following error:

Uncaught EXCEPTION: Error during evaluation of "mousemove"

ORIGINAL EXCEPTION: ObjectUnsubscribedError

ERROR CONTEXT: EventEvaluationErrorContext

The issue seems to arise from the inability to cancel the mousemove subscription properly, leading to repeated errors with subsequent mouse movements. Any suggestions on what might be causing this issue? Is there a more elegant solution to handle this problem?

Answer №1

Your issue may stem from the difference between using unsubscribe() and remove(sub : Subscription) on an EventEmitter. To solve this problem without relying on subscriptions (except those created by a @HostListener), I have made some modifications to your code. It is advisable to place your mouseup event on either the document or window to avoid strange behavior if you release your mouse outside the canvas.

Note: The following code has not been tested

@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;
    }
}

Answer №2

The issue you're facing is that the code lacks reactivity. In reactive programming, all behaviors should be defined during decoration time with just one required subscription.

For illustration, take a look at this sample: Angular2/rxjs mouse translation/rotation

import {Component, NgModule, OnInit, ViewChild} from '@angular/core'
import {BrowserModule, ElementRef, MouseEvent} from '@angular/platform-browser'
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMapTo';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/startWith';

@Component({
  selector: 'my-app',
  styles: [`
  canvas{
    border: 1px solid red;
  }`],
  template: `
    <div>
      <h2>translate/Rotate by mouse</h2>
      <canvas #canvas id="3dview"></canvas>
      <p>Translate by delta: {{relativeTo$|async|json}}</p>
      <p>Rotate by angle: {{rotateToAngle$|async|json}}</p>
    </div>
  `
})
export class App extends OnInit {

    @ViewChild('canvas') 
    canvas: ElementRef;

    relativeTo$: Observable<{dx:number, dy:number, start: MouseEvent}>;
    rotateToAngle$: Observable<{angle:number, start: MouseEvent}>;

    ngOnInit() {
      const canvasNE = this.canvas.nativeElement;

      const mouseDown$ = Observable.fromEvent(canvasNE, 'mousedown');
      const mouseMove$ = Observable.fromEvent(canvasNE, 'mousemove');
      const mouseUp$ = Observable.fromEvent(canvasNE, 'mouseup');

      const moveUntilMouseUp$= mouseMove$.takeUntil(mouseUp$);
      const startRotate$ = mouseDown$.switchMapTo(moveUntilMouseUp$.startWith(null));

      const relativePoint = (start: MouseEvent, end: MouseEvent): {x:number, y:number} => 
      (start && end && {
        dx:start.clientX - end.clientX,
        dy: start.clientY - end.clientY,
        start: start
      } || {});

      this.relativeTo$ = startRotate$
        .combineLatest(mouseDown$)
        .map(arr => relativePoint(arr[0],arr[1]));

      this.rotateToAngle$ = this.relativeTo$
        .map((tr) => ({angle: Math.atan2(tr.dy, tr.dx), start: tr.start}));

//      this.relativeTo$.subscribe(console.log.bind(console,'rotate:'));
//      this.rotateToAngle$.subscribe(console.log.bind(console,'rotate 0:'));
    }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Discovering the best way to utilize pagination for searching all data within Angular 8

Hey there, good morning everyone! I'm currently working on an Angular 8 app that showcases a table filled with data from a database. This table comes equipped with a search box and a pagination feature using the "Ng2SearchPipeModule" and "JwPaginatio ...

Each Tab in Ionic2 can have its own unique side menu that opens when selected

In my ionic2 app, I wanted to implement a unique side menu for each of my tabs. Here is what I attempted: I used the command ionic start appname tabs --v2 to create the initial structure. Next, I decided to turn both home.html and contact.html (generated ...

Using TypeScript to define callback functions within the Cordova.exec method

I'm encountering an issue with the TypeScript definition for Cordova. The codrova.d.ts file doesn't allow for any function arguments in the success-callback and error-callback. To better illustrate my problem, here's a small example: Here ...

Utilizing ReactJS and TypeScript to retrieve a random value from an array

I have created a project similar to a "ToDo" list, but instead of tasks, it's a list of names. I can input a name and add it to the array, as well as delete each item. Now, I want to implement a button that randomly selects one of the names in the ar ...

Are MobX Observables interconnected with RxJS ones in any way?

Is the usage of RxJs observables in Angular comparable to that in React and MobX? I'm struggling to find information on this topic. ...

Switching between two distinct templateUrls within an Angular 6 component

When working with Angular 6, we are faced with having two different templates (.html) for each component, and the need to change them based on the deployment environment. We are currently exploring the best practices for accomplishing this task. Some pos ...

Utilizing Typescript to pass props to a material-ui button enclosed in styled-components

After setting up my react project using the typescript template, I decided to style the material-ui Button component using the 'styled' method from the styled-components library as shown below: import React from 'react'; import styled f ...

Send a collection of objects by submitting a form

I have a component with the following html code. I am attempting to dynamically generate a form based on the number of selected elements, which can range from 0 to N. <form #form="ngForm" id="formGroupExampleInput"> <div class="col-xs-5 col-md- ...

Combine Two Values within Model for Dropdown Menu

I am currently facing a situation where I have a select box that displays a list of users fetched from the backend. The select box is currently linked to the name property of my ng model. However, each user object in the list also contains an email proper ...

Is there an rxjs operator that includes both on-next and on-error callbacks?

Is there a similar function to promise.then(onNextCallback,onErrorCallback) in rxjs that I can use? I've already tried alternatives like pipe(concatMap(),catchError) but they are not what I am looking for. ...

Utilizing Angular 5 routerLink for linking to absolute paths with hash symbols

I am facing an issue with a URL that needs to be opened in a new tab. Unfortunately, Angular generates this URL without the # symbol. Currently, we have implemented the following: <!-- HTML --> <a title="Edit" [routerLink] = "['/object/objec ...

How to arrange table data in Angular based on th values?

I need to organize data in a table using <th> tags for alignment purposes. Currently, I am utilizing the ng-zorro table, but standard HTML tags can also be used. The data obtained from the server (via C# web API) is structured like this: [ { ...

PageObjectModel Playwright, execute the locator().all() function - 'The execution context has been terminated, possibly due to navigating to another...'

Hey there, I'm currently working on a basic test using POM. Here is a snippet from one of my PageObjects Class: import { Expect, Page, Locator } from "@playwright/test"; export class InventoryPage { readonly page: Page; readonly addToC ...

Is there a way for me to view the output of my TypeScript code in an HTML document?

This is my HTML *all the code has been modified <div class="testCenter"> <h1>{{changed()}}</h1> </div> This is my .ts code I am unsure about the functionality of the changed() function import { Component, OnInit } f ...

I'm experiencing unexpected behavior with the use of Mat-Spinner combined with async in Angular 12, particularly when using the rxjs function

I am relatively new to rxjs and it's possible that I'm using the wrong function altogether. Currently, I'm working on a .NET Core 3.1 backend and implementing a two-second delay for testing purposes. I have a service call that I need to mock ...

Collaborate on an Angular2 codebase for both a web application and a hybrid application

I am considering creating a mobile app based on my existing Angular2 web app codebase. After researching, I have found two possible options - Ionic2 and NativeScript. Upon further investigation, it appears that both Ionic2 and NativeScript have their own ...

Dynamic Text Labels in Treemap Visualizations with Echarts

Is it possible to adjust the text size dynamically based on the size of a box in a treemap label? I haven't been able to find a way to do this in the documentation without hardcoding it. Click here for more information label: { fontSize: 16 ...

Ways to enable components to access a string dependency token

I'm currently developing an Angular application that utilizes Angular Universal for server-side rendering functionality. One interesting aspect of my project involves passing a string dependency token as a provider within the providers array in serve ...

Angular4 - Div with ngIf doesn't respond to click event

I'm attempting to add a click event to a specific div. Within this div, there is another div that is dynamically generated based on a Boolean condition that I receive as input. Unfortunately, in this case, the click event is only functioning when clic ...

What is the correct way to declare a class as global in TypeScript?

To prevent duplication of the class interface in the global scope, I aim to find a solution that avoids redundancy. The following code snippet is not functioning as intended: lib.ts export {} declare global { var A: TA } type TA = typeof A class A { ...