Create a directive for free dragging in Angular

Post Editor

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.

9 min read
0 comments
post

Create a directive for free dragging in Angular

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.

post
post
9 min read
0 comments
0 comments

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
Link to this section

1 Create a basic free dragging directive
Link to this section

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

1.1 Create a workspace
Link to this section

<>Copy
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.

1.2 Create shared module
Link to this section

<>Copy
ng g m shared

1.3.1 Create free dragging directive
Link to this section

<>Copy
ng g d shared/free-dragging

1.3.2 Export the directive
Link to this section

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

<>Copy
// 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
Link to this section

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:

<>Copy
// 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:

<>Copy
// 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 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 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
Link to this section

Replace the content with below:

<>Copy
// 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:

<>Copy
ng serve

and see the output:

Output after step 4Output after step 4
Output after step 4

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
Link to this section

We will add support for drag handle by creating one more directive and accessing it with @ContentChild in our main directive.

2.1 Create a directive for drag handle
Link to this section

<>Copy
ng g d shared/free-dragging-handle

2.2 Export it from shared module
Link to this section

<>Copy
// 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
Link to this section

We will just need drag handle's element to do the next stuff, let's use ElementRef for the same:

<>Copy
// 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
Link to this section

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:

<>Copy
// 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.

2.5 Update AppComponent
Link to this section

<>Copy
// 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 7output after step 7
output after step 7

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 viewallowing to drag beyond view
allowing to drag beyond view

3. Add Support for Dragging Boundary
Link to this section

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

3.1 Update the directive
Link to this section

For boundary support, we will go like this:

  1. Add an @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, if not throw error.
  3. Use boundary-element's layout height and width to adjust the position of dragged element.
<>Copy
// 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.

<>Copy
// src/styles.css html, body { height: 100%; }

Let's see the output now:

Content imageContent image
final result

That's it! Kudos... ???

Conclusion
Link to this section

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 using 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:

Code is available at: https://github.com/shhdharmen/angular-free-draggingCode is available at: https://github.com/shhdharmen/angular-free-dragging

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

Credits
Link to this section

While writing this article, I took references from code snippets present at w3schools and stackoverflow.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

author_image

About the author

Dharmen Shah

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

Featured articles