Building an Absolute Drag Directive in Ionic 2

Recently I sat down to record a screencast of myself building out a new directive in Ionic 2. It was quite a simplistic directive, but the intent was to build it from start to finish with no prior planning involved so that I could show people the types of roadblocks I run into and the problem-solving approach I take when building something.

Although most of the videos I create are unscripted, there is at least some level of planning involved and I know more or less what I’m doing before I start recording. I think this can create the perception that I, and others who create educational content, just know how to do everything without ever getting stuck or having to look things up.

I will release that video soon (make sure to subscribe to my YouTube channel if you want to be notified when it is out), but this post will detail the directive that I ended up creating. I will tie up a few loose ends, fix a couple of problems, and provide more of a tutorial style walkthrough of building the directive.

We will walk through building a directive in Ionic 2 that can be added to any element to make it draggable. This is a very simple implementation of drag functionality and relies on absolutely positioning elements, so it may not be suitable for more advanced dragging requirements (this was mostly just intended to be a fun walkthrough of building a directive).

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

This tutorial will not cover the basics of creating directives so you may want some background knowledge before you continue. In short, a directive is something that can be created and then added to an element to modify its behaviour (unlike a component, which creates an entirely new element).

1. Generate a New Ionic 2 Application

Let’s start by creating a new Ionic 2 application, we are just going to use the blank tempate and add a bunch of junk elements to it to drag around.

Run the following command to generate a new Ionic 2 application:

ionic start ionic2-drag blank --v2

Once that has finished generating, you should make it your working directory with the following command:

cd ionic2-drag

We will also generate our directive now as well.

Run the following command to generate the directive:

ionic g directive AbsoluteDrag

and finally, we will add it to the app.module.ts file to make it available throughout the application.

Modify src/app/app.module.ts to reflect the following:

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AbsoluteDrag } from '../components/absolute-drag/absolute-drag';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AbsoluteDrag
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

2. Build the Absolute Drag Directive

First, let’s add the code for the directive and then we will talk through it.

Modify src/components/absolute-drag/absolute-drag.ts to reflect the following:

import { Directive, Input, ElementRef, Renderer } from '@angular/core';
import { DomController } from 'ionic-angular';

@Directive({
  selector: '[absolute-drag]'
})
export class AbsoluteDrag {

    @Input('startLeft') startLeft: any;
    @Input('startTop') startTop: any;

    constructor(public element: ElementRef, public renderer: Renderer, public domCtrl: DomController) {

    }

    ngAfterViewInit() {

        this.renderer.setElementStyle(this.element.nativeElement, 'position', 'absolute');
        this.renderer.setElementStyle(this.element.nativeElement, 'left', this.startLeft + 'px');
        this.renderer.setElementStyle(this.element.nativeElement, 'top', this.startTop + 'px');

        let hammer = new window['Hammer'](this.element.nativeElement);
        hammer.get('pan').set({ direction: window['Hammer'].DIRECTION_ALL });

        hammer.on('pan', (ev) => {
          this.handlePan(ev);
        });

    }

    handlePan(ev){

        let newLeft = ev.center.x;
        let newTop = ev.center.y;

        this.domCtrl.write(() => {
            this.renderer.setElementStyle(this.element.nativeElement, 'left', newLeft + 'px');
            this.renderer.setElementStyle(this.element.nativeElement, 'top', newTop + 'px');
        });

    }

}

Our directive has a selector of [absolute-drag] which means that we can add absolute-drag to any element in order to invoke this directives functionality on it.

We set up a couple of @Input member variables at the top, which will allow us to pass in startLeft and startTop values. As I mentioned, elements that have this directive applied will be absolutely positioned, so this allows you to specify where its starting position should be before it is dragged.

The ngAfterViewInit will run once the view has finished loading, and it is used to do some set up for the directive. We set the host element to have an absolute, and we set the initial position, by using the setElementStyle method on the Renderer.

Next, we set up a pan listener on the host element using Hammer. Hammer is actually used by default by Ionic, and Ionic even already has the pan event available to capture, but it does not have vertical panning enabled by default which we require. So, we create our own pan recogniser and enable all pan directions using DIRECTION_ALL. We then create a handler for those pan events called handlePan.

The handlePan function is responsible for figuring out where the user is panning, and then updating the left and top values to reflect that. It is important that we run these updates inside of the DomController’s write method, as this will allow Ionic to schedule the updates in the most efficient way possible (rather than forcing it to update every time as soon as we call the setElementStyle function). This will lead to much smoother performance, and you should do this just about any time that you are updating something in the DOM.

That’s about all there is to this directive, now we just have to use it.

3. Using the Absolute Drag Directive

To use the directive, all we have to do is add the absolute-drag directive to any element in any of our templates. Let’s look at an example.

Modify src/pages/home/home.html to reflect the following:

<ion-header absolute-drag>
  <ion-navbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content fullscreen>

    <button ion-button absolute-drag startLeft="20" startTop="100">Drag Me!</button>

    <button ion-button absolute-drag>Drag Me!</button>

    <ion-card absolute-drag startLeft="200" startTop="200">
        <ion-card-content>
            Drag me!
        </ion-card-content>
    </ion-card>

    <p absolute-drag>
        Drag me!
    </p>

    <img src="http://placehold.it/75" absolute-drag />

</ion-content>

We have basically just added a bunch of elements and then added absolute-drag to them (we have also added starting positions to some). You should now be able to drag all of these elements around the screen. You can even do some funky stuff with it like adding it to the navigation bar to make that draggable (not that you would likely ever want to do that).

Drag Directive in Ionic 2

Summary

This is a bit of a quirky example, but we have walked through building a new directive in Ionic 2 from start to finish. I will release the original video of me building this soon so you can see the build process in more detail, including the multiple failed attempts it took to get there.

Check out my latest videos: