Integrating InteractJS in Angular

February 18, 2020 in Tooling

A woman touching the screen of her smartphone

Resizing, dragging, dropping and general multi-touch gestures can be a critical asset for web applications trying to provide first class user experience to their users. In this article we'll have a look at interactjs. We'll examine what it is, what it provides us with and how we can best integrate it into new or already existing Angular projects.

Why interactjs?

Interactjs is not the only library out in the wild providing us with first class drag and drop functionality, there is still plain HTML5 functionality, but also libraries like draggable.js, dragula, sortable and magnet.js could be used to enable drag and drop functionality.

What makes interactjs stand out from the crowd is that it neither does presume behaviour upfront, nor does it do DOM manipulations out of the box.

Instead, it's basically a set of events that will be triggered upon execution of certain resize or drag and drop actions taken by the user.

This workflow is particularly great for JavaScript Single Page Applications like Angular or other, which really don't like it when other libraries are messing with their DOM.

So it's up to us, the developers, to define the behaviour that best suits our use case whenever those events are triggered.

Installation

Installation of interactjs is pretty straight forward, we just install the node module from npm.

npm i interactjs

Yeah, done. Now we have it lying in our node_modules folder, waiting to be put to action for some drag and drop fun!

Integration

Being the good developers we are, we are of course trying to abstract away the direct interactjs dependency into something a little more abstract and angular-ish.

So we'll define 2 angular directives called draggable and droppable. Fancy expressive naming, there you go.

The Draggable Directive

Here's the draggable one we've come up with:

import {Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output} from '@angular/core';
import * as interact from 'interactjs';

@Directive({
  selector: '[appDraggable]'
})
export class DraggableDirective implements OnInit {

  @Input()
  model: any;

  @Input()
  options: any;

  @Output() 
  draggableClick = new EventEmitter();

  private currentlyDragged = false;

  constructor(private element: ElementRef) {}

  @HostListener('click', ['$event'])
  public onClick(event: any): void {
    if (!this.currentlyDragged) {
      this.draggableClick.emit();
    }
  }

  ngOnInit(): void {
    interact(this.element.nativeElement)
      .draggable(Object.assign({}, this.options || {}))
      .on('dragmove', (event) => {
        const target = event.target;
        const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
        const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

        target.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
        target.setAttribute('data-x', x);
        target.setAttribute('data-y', y);

        target.classList.add('getting-dragged');
        this.currentlyDragged = true;
        (window as any).dragData = this.model;
      })
      .on('dragend', (event) => {
        event.target.style.transform = 'none';
        event.target.removeAttribute('data-x');
        event.target.removeAttribute('data-y');
        event.target.classList.remove('getting-dragged');
        setTimeout(() => {
          (window as any).dragData = null;
          this.currentlyDragged = false;
        });
      });
  }
}

Well, that's a lot of code. Let's go through it step by step to see what it actually does.

First part, we are importing the interactjs dependency, obviously.

After that, we define 3 public fields in our directive being model, options and draggableClick.

  • model is used to pass some value, entity, dto or, as the name implies, model to the drag event, which will then be handed over to the element handling the drop of the dragged element.
  • options allows us to declaratively pass specific interactjs options through the directive to the dragevent.
  • draggableClick is a specific click event handler just like the one angular provides us with, with one exception which is that this click handler is aware that the element can be dragged, so it will only emit when the element is clicked and not dragged. So no false-positives on the click event any more when dragging an element.

Having talked about the exposed fields, let's check out which methods we have defined here

  • onClick - This is the event handler for the draggableClick EventEmitter implementation, we are just checking if the element is dragged or has been dragged.
  • ngOnInit - This is where the dragging magic happens.

In the ngOnInit Angular Lifecycle method we are setting up our dragging behaviour.

First, we are telling interactjs about the element to be dragged, using the ElementRef service that we injected.

After that, we are passing any interactjs specific options that might have been defined declaratively to interactjs.

And after that, we are using the events we praised so much in our Why interactjs? Section.

We are listening for the dragmove and dragend events of interactjs, and are then manipulating the HTML element accordingly.

But as described before, we are not resorting the DOM or anything like that, we are just putting some subtle transform CSS styles and a CSS class. Using this class we can further style the element to give the user good feedback about the dragging event that is happening.

Also, we are saving the data that has been passed to the directive in the window of the browser, so that the "drop" directive is able to fetch it. If we wouldn't be able to access the window we could also put it in localStorage or similar, it just should be a well-known place where the drop directive is able to fetch the data.

The Droppable Directive

Having spoken about the draggable directive, let's see it's counter-part, the droppable:

import {Directive, ElementRef, EventEmitter, Input, OnInit, Output} from '@angular/core';
import * as interact from 'interactjs';

@Directive({
  selector: '[appDroppable]'
})
export class DroppableDirective implements OnInit {

  @Input()
  options: any;

  @Output()
  dropping: EventEmitter<any> = new EventEmitter();

  constructor(private elementRef: ElementRef) {}

  ngOnInit(): void {
    interact(this.elementRef.nativeElement)
      .dropzone(Object.assign({}, this.options || {}))
      .on('dropactivate', event => event.target.classList.add('can-drop'))
      .on('dragenter', event => {

        const draggableElement = event.relatedTarget;
        const dropzoneElement = event.target;

        dropzoneElement.classList.add('can-catch');
        draggableElement.classList.add('drop-me');

      })
      .on('dragleave', event => {
        event.target.classList.remove('can-catch', 'caught-it');
        event.relatedTarget.classList.remove('drop-me');
      })
      .on('drop', event => {
        const model = (window as any).dragData;

        if (typeof (model) === 'object') {
          this.dropping.emit(model);
        }
        event.target.classList.add('caught-it');

        if ((window as any).document.selection) {
          (window as any).document.selection.empty();
        } else {
          window.getSelection().removeAllRanges();
        }
      })
      .on('dropdeactivate', event => {
        event.target.classList.remove('can-drop');
        event.target.classList.remove('can-catch');
      });
  }
}

Similar to the draggable directive before, we are again defining some public fields being:

  • options Just like before, this is used to declaratively pass along interactjs specific options to the drop event configuration.
  • dropping This EventEmitter is being called as soon as the dragged element has been dropped, carrying the model passed to the draggable directive.

In the ngOnInit method, we are again initializing interactjs with the the current element using the ElementRef service, passing along the declared options if any, and then again we are listening to the events interactjs provides us with.

  • dropactivate This event is used to set the class can-drop, which should be styled to indicate the user that the dragged element can be dropped at the position of that element.
  • dragenter Should be used to indicate the user the element can possibly drop right now, because the dropzone is able to handle dragged element.
  • dragleave Reverses dragenter, which means if the user only dragged "over" that element, we are removing all the styles of dragenter again.
  • drop as the most important event, is extracting the model data from the windows object, adds a CSS class to indicate the user that he dropped the dragged element successfully, and removes all the selections that some browsers might perform during drop events.
  • dropdeactivate removes all the drop specific classes that symbolize the user that a drop could happen at the end.

Well, that's basically it, that is how we can abstract away the direct interactjs behaviour into angular directives. So... let's use them!

Usage

First, let's define something draggable, shall we?

<div class="task" (draggableClick)="openDetail(task)" [model]="task" appDraggable">
    I'm a draggable task
</div>

As we can see, we are using an ordinary <div> element, and apply draggable superpowers to it by applying our directive appDraggable.

This enables us to use the [model] binding to pass the data to the directive, and the draggableClick event listener to f.e. open the detail page of our task if we wan to.

Next up, dropping like it's hot! (Sorry, couldn't help it)

<div appDroppable [options]="{ accept: '.task' }" (dropping)="assignTask($event, user)">
    I'm a task handling developer div
</div>

By applying the appDroppable directive here, we are as above able to use the methods we declared in our JavaScript above, meaning we are utilizing the dropping event listener this time to accept the "dropped" data and use it for good from this point on. Here we can also see that we are utilizing the [options] binding which directs interactjs to only fire dropactivate events if the element hovering over the drop zone has the class task attached to it.

So we got the html, now there is only one thing left... Yes, the drop-handling JavaScript of course. Have no fear, it's extremely simple too.

assignTask(task: TaskDTO, user: UserDTO) {
    console.log("This is our dragged task model: ");
    console.log(task);

    console.log("This is our user: ");
    console.log(user)
}

As we said, straight forward. The event passed is just the model we have dragged, and the dropped.

Congratulations

Nicely done. Now go ahead and drag and drop the shizzles out of your new fancy web application using interactjs and Angular!