Create a directive for free dragging in Angular
Tags: angular, draggable, directive, typescript
Published: 2020-11-25 05:35:47

---
title: Create a directive for free dragging in Angular
published: true
description: In this article, we will learn how to create a directive in Angular that will allow us to freely drag any element, without using any 3rd party libraries.
tags: angular,draggable,directive,typescript
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/ilin5wcla5byjs0xwb62.jpg
canonical_url: https://indepth.dev/drag-drop-directive
---

In this article, we will learn how to create a directive in Angular that will allow us to freely drag any element, without using any 3rd party libraries.

# Let's start coding

## 1 Create a basic free dragging directive

We will start by creating a basic and simple directive and then will continue to add more features.

### 1.1 Create a workspace

```
npm i -g @angular/cli
ng new angular-free-dragging --defaults --minimal
```

> Do not use `--minimal` option in production applications, it creates a workspace without any testing frameworks. You can read more about [CLI options](https://angular.io/cli/new#options).

### 1.2 Create shared module

```
ng g m shared
```

### 1.3.1 Create free dragging directive

```
ng g d shared/free-dragging
```

### 1.3.2 Export the directive

Once it's created, add it in the exports array of _shared_ module:

```typescript
// src/app/shared/shared.module.ts

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FreeDraggingDirective } from "./free-dragging.directive";

@NgModule({
  declarations: [FreeDraggingDirective],
  imports: [CommonModule],
  exports: [FreeDraggingDirective], // Added
})
export class SharedModule {}
```

### 1.3.3 Free dragging logic

To have a free dragging, we are going to do below:

1.  Listen for `mousedown` event on element. This will work as _drag-start_ trigger.
2.  Listen for `mousemove` event on document. This will work as _drag_ trigger. It will also update the position of element based on mouse pointer.
3.  Listen for `mouseup` event on document. This will work as _drag-end_ trigger. With this, we will stop listening to `mousemove` event.

For all above listeners, we will create observables. But first, let's setup our directive:

```typescript
// src/app/shared/free-dragging.directive.ts

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements OnInit, OnDestroy {
  private element: HTMLElement;

  private subscriptions: Subscription[] = [];

  constructor(
    private elementRef: ElementRef,
    @Inject(DOCUMENT) private document: any
  ) {}

  ngOnInit(): void {
    this.element = this.elementRef.nativeElement as HTMLElement;
    this.initDrag();
  }

  initDrag(): void {
    // main logic will come here
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach((s) => s.unsubscribe());
  }
}

```

In above code, mainly we are doing 3 things:

1.  Getting native HTML element, so that we can change it's position later on.
2.  Initiating all dragging operations, we will see this in detail soon.
3.  At the time of destroying, we are unsubscribing to make resources free.

Let's write dragging functions:

```typescript
// src/app/shared/free-dragging.directive.ts

...

  initDrag(): void {
    // 1
    const dragStart$ = fromEvent<MouseEvent>(this.element, "mousedown");
    const dragEnd$ = fromEvent<MouseEvent>(this.document, "mouseup");
    const drag$ = fromEvent<MouseEvent>(this.document, "mousemove").pipe(
      takeUntil(dragEnd$)
    );

    // 2
    let initialX: number,
      initialY: number,
      currentX = 0,
      currentY = 0;

    let dragSub: Subscription;

    // 3
    const dragStartSub = dragStart$.subscribe((event: MouseEvent) => {
      initialX = event.clientX - currentX;
      initialY = event.clientY - currentY;
      this.element.classList.add('free-dragging');

      // 4
      dragSub = drag$.subscribe((event: MouseEvent) => {
        event.preventDefault();

        currentX = event.clientX - initialX;
        currentY = event.clientY - initialY;

        this.element.style.transform =
          "translate3d(" + currentX + "px, " + currentY + "px, 0)";
      });
    });

    // 5
    const dragEndSub = dragEnd$.subscribe(() => {
      initialX = currentX;
      initialY = currentY;
      this.element.classList.remove('free-dragging');
      if (dragSub) {
        dragSub.unsubscribe();
      }
    });

    // 6
    this.subscriptions.push.apply(this.subscriptions, [
      dragStartSub,
      dragSub,
      dragEndSub,
    ]);
  }

...
```

1.  We are creating 3 observables for the listeners which we saw earlier using the `[fromEvent](https://rxjs.dev/api/index/function/fromEvent)` function.
2.  Then we are creating some helper variables, which will be needed in updating the position of our element.
3.  Next we are listening for `mousedown` event on our element. Once user presses mouse, we are storing initial position and we are also adding a class `free-dragging` which will add a nice shadow to element.
4.  We want to move the element only if user has clicked it, that why we are listening for `mousemove` event inside the subscriber of `mousedown` event. When user moves the mouse, we are also updating it's position using [transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) property.
5.  We are then listening for `mouseup` event. In this we are again updating initial positions so that next drag happens from here. And we are removing the `free-dragging` class.
6.  Lastly, we are pushing all the subscriptions, so that we can unsubscribe from all in `ngOnDestroy` .

It's time to try this out in _AppComponent._

### 1.3.4 Update AppComponent

Replace the content with below:

```typescript
// src/app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  // 1 use directive
  template: ` <div class="example-box" appFreeDragging>Drag me around</div> `,
  // 2 some helper styles
  styles: [
    `
      .example-box {
        width: 200px;
        height: 200px;
        border: solid 1px #ccc;
        color: rgba(0, 0, 0, 0.87);
        cursor: move;
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        background: #fff;
        border-radius: 4px;
        position: relative;
        z-index: 1;
        transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
        box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
          0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
      }

      .example-box.free-dragging {
        box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
          0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
      }
    `,
  ],
})
export class AppComponent {}

```

The above code is simple and clear enough. Let's run it:

```bash
ng serve
```

and see the output:

![Output after step 4](https://admin.indepth.dev/content/images/2020/11/ezgif.com-gif-maker--4-.gif)

In current directive, user can drag element by pressing and moving mouse anywhere in the element. Drawback of this is, difficultly in other actions, like selecting the text. And in more practical scenarios, like widgets, you will need an handle for easiness in dragging.

## 2. Add Support for Drag Handle

We will add support for drag handle by creating one more directive and accessing it with [`@ContentChild`](https://angular.io/api/core/ContentChild) in our main directive.

### 2.1 Create a directive for drag handle

```bash
ng g d shared/free-dragging-handle
```

### 2.2 Export it from shared module

```typescript
// src/app/shared/shared.module.ts

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FreeDraggingDirective } from "./free-dragging.directive";
import { FreeDraggingHandleDirective } from './free-dragging-handle.directive';

@NgModule({
  declarations: [FreeDraggingDirective, FreeDraggingHandleDirective],
  imports: [CommonModule],
  exports: [FreeDraggingDirective, FreeDraggingHandleDirective], // Modified
})
export class SharedModule {}

```

### 2.3 Return ElementRef from drag handle

We will just need drag handle's element to do the next stuff, let's use [`ElementRef`](https://angular.io/api/core/ElementRef) for the same:

```typescript
// src/app/shared/free-dragging-handle.directive.ts

import { Directive, ElementRef } from "@angular/core";

@Directive({
  selector: "[appFreeDraggingHandle]",
})
export class FreeDraggingHandleDirective {
  constructor(public elementRef: ElementRef<HTMLElement>) {} // Modified
}

```

### 2.4 Drag with handle

The logic goes like this:

1.  Get child drag handle-element from main element
2.  Listen for `mousedown` event on handle-element. This will work as _drag-start_ trigger.
3.  Listen for `mousemove` event on document. This will work as _drag_ trigger. It will also update the position of main-element (and not only handle-element) based on mouse pointer.
4.  Listen for `mouseup` event on document. This will work as _drag-end_ trigger. With this, we will stop listening to `mousemove` event.

So basically, the only change would be to change the element, on which we will listen for `mousedown` event.

Let's get back to coding:

```typescript
// src/app/shared/free-dragging.directive.ts

...

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements AfterViewInit, OnDestroy {

  private element: HTMLElement;

  private subscriptions: Subscription[] = [];

  // 1 Added
  @ContentChild(FreeDraggingHandleDirective) handle: FreeDraggingHandleDirective;
  handleElement: HTMLElement;

  constructor(...) {}

  // 2 Modified
  ngAfterViewInit(): void {
    this.element = this.elementRef.nativeElement as HTMLElement;
    this.handleElement = this.handle?.elementRef?.nativeElement || this.element;
    this.initDrag();
  }

  initDrag(): void {
    // 3 Modified
    const dragStart$ = fromEvent<MouseEvent>(this.handleElement, "mousedown");

    // rest remains same

  }

  ...

}
```

We are doing the same as what is explained in logic before the code. Please note that, now instead of `ngOnInit` we are using `ngAfterViewInit`, because we want to make sure that component's view is fully initialized and we can get the `FreeDraggingDirective` if present. You can read more about the same at [Angular - Hooking into the component lifecycle](https://angular.io/guide/lifecycle-hooks).

### 2.5 Update AppComponent

```typescript
// src/app/app.component.ts

@Component({
  selector: "app-root",
  template: `
    <!-- 1 use directive -->
    <div class="example-box" appFreeDragging>
      I can only be dragged using the handle

      <!-- 2 use handle directive -->
      <div class="example-handle" appFreeDraggingHandle>
        <svg width="24px" fill="currentColor" viewBox="0 0 24 24">
          <path
            d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"
          ></path>
          <path d="M0 0h24v24H0z" fill="none"></path>
        </svg>
      </div>
    </div>
  `,
  // 3 helper styles
  styles: [
    `
      .example-box {
        width: 200px;
        height: 200px;
        padding: 10px;
        box-sizing: border-box;
        border: solid 1px #ccc;
        color: rgba(0, 0, 0, 0.87);
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        background: #fff;
        border-radius: 4px;
        position: relative;
        z-index: 1;
        transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
        box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
          0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
      }

      .example-box.free-dragging {
        box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
          0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
      }

      .example-handle {
        position: absolute;
        top: 10px;
        right: 10px;
        color: #ccc;
        cursor: move;
        width: 24px;
        height: 24px;
      }
    `,
  ],
})
export class AppComponent {}
```

Let's look at the output:

![output after step 7](https://admin.indepth.dev/content/images/2020/11/ezgif.com-gif-maker--5-.gif)

Great, we have almost achieved what we need.

But, there is still one problem with it. It is allowing user to move element outside the view:

![allowing to drag beyond view](https://admin.indepth.dev/content/images/2020/11/ezgif.com-gif-maker--7-.gif)

## 3. Add Support for Dragging Boundary

It's time to add support for boundary. Boundary will help user keep the element inside the desired area.

### 3.1 Update the directive

For boundary support, we will go like this:

1.  Add an [`@Input`](https://angular.io/api/core/Input) to set custom boundary-element query. By default, we will keep it at `body`.
2.  Check if we can get the boundary-element using [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), if not throw error.
3.  Use boundary-element's layout height and width to adjust the position of dragged element.

```typescript
// src/app/shared/free-dragging.directive.ts

...

@Directive({
  selector: "[appFreeDragging]",
})
export class FreeDraggingDirective implements AfterViewInit, OnDestroy {

  ...

  // 1 Added
  private readonly DEFAULT_DRAGGING_BOUNDARY_QUERY = "body";
  @Input() boundaryQuery = this.DEFAULT_DRAGGING_BOUNDARY_QUERY;
  draggingBoundaryElement: HTMLElement | HTMLBodyElement;

  ...

  // 2 Modified
  ngAfterViewInit(): void {
    this.draggingBoundaryElement = (this.document as Document).querySelector(
      this.boundaryQuery
    );
    if (!this.draggingBoundaryElement) {
      throw new Error(
        "Couldn't find any element with query: " + this.boundaryQuery
      );
    } else {
      this.element = this.elementRef.nativeElement as HTMLElement;
      this.handleElement =
        this.handle?.elementRef?.nativeElement || this.element;
      this.initDrag();
    }
  }

  initDrag(): void {
    ...

    // 3 Min and max boundaries
    const minBoundX = this.draggingBoundaryElement.offsetLeft;
    const minBoundY = this.draggingBoundaryElement.offsetTop;
    const maxBoundX =
      minBoundX +
      this.draggingBoundaryElement.offsetWidth -
      this.element.offsetWidth;
    const maxBoundY =
      minBoundY +
      this.draggingBoundaryElement.offsetHeight -
      this.element.offsetHeight;

    const dragStartSub = dragStart$.subscribe((event: MouseEvent) => {
      ...

      dragSub = drag$.subscribe((event: MouseEvent) => {
        event.preventDefault();

        const x = event.clientX - initialX;
        const y = event.clientY - initialY;

        // 4 Update position relatively
        currentX = Math.max(minBoundX, Math.min(x, maxBoundX));
        currentY = Math.max(minBoundY, Math.min(y, maxBoundY));

        this.element.style.transform =
          "translate3d(" + currentX + "px, " + currentY + "px, 0)";
      });
    });

    const dragEndSub = dragEnd$.subscribe(() => {
      initialX = currentX;
      initialY = currentY;
      this.element.classList.remove("free-dragging");
      if (dragSub) {
        dragSub.unsubscribe();
      }
    });

    this.subscriptions.push.apply(this.subscriptions, [
      dragStartSub,
      dragSub,
      dragEndSub,
    ]);
  }
}
```

You will also need to set `body`'s height to 100%, so that you can drag the element around.

```css
// src/styles.css

html,
body {
  height: 100%;
}

```

Let's see the output now:

![](https://admin.indepth.dev/content/images/2020/11/ezgif.com-gif-maker--9--1.gif)

That's it! Kudos... 🎉😀👍

## Conclusion

Let's quickly revise what we did:

✔️ We created a directive for free dragging

✔️ Then added support for drag handle, so that user can perform other actions on element

✔️ Lastly, we also added boundary element, which helps to keep element to be dragged insider a particular boundary

✔️ And all of it without any 3rd party libraries 😉

You can still add many more features to this, I will list a few below:

1. Locking axes - allow user to drag only in horizontal or vertical direction
2. Events - generate events for each action, like drag-start, dragging and drag-end
3. Reset position - move the drag to it's initial position

You can use this dragging feature in many cases, like for a floating widget, chat box, help & support widget, etc. You can also build a fully-featured editor, which supports elements (like headers, buttons, etc.) to be dragged around.

---

All of above code is available on Github:

{% github shhdharmen/angular-free-dragging %}

Thanks for reading this article. Let me know your thoughts and feedback in comments section.

### Credits

While writing this article, I took references from code snippets present at  [w3schools](https://www.w3schools.com/howto/howto_js_draggable.asp)  and  [stackoverflow](https://stackoverflow.com/a/5189066/3339907).