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.
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:
To have a free dragging, we are going to do below:
Listen for mousedown event on element. This will work as drag-start trigger.
Listen for mousemove event on document. This will work as drag trigger. It will also update the position of element based on mouse pointer.
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:
// src/app/shared/free-dragging.directive.ts@Directive({selector:"[appFreeDragging]",})exportclassFreeDraggingDirectiveimplementsOnInit,OnDestroy{privateelement:HTMLElement;privatesubscriptions:Subscription[]=[];constructor(privateelementRef:ElementRef,@Inject(DOCUMENT)privatedocument:any){}ngOnInit():void{this.element=this.elementRef.nativeElementasHTMLElement;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:
Getting native HTML element, so that we can change it's position later on.
Initiating all dragging operations, we will see this in detail soon.
At the time of destroying, we are unsubscribing to make resources free.
We are creating 3 observables for the listeners which we saw earlier using the [fromEvent](https://rxjs.dev/api/index/function/fromEvent) function.
Then we are creating some helper variables, which will be needed in updating the position of our element.
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.
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.
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.
Lastly, we are pushing all the subscriptions, so that we can unsubscribe from all in ngOnDestroy .
The above code is simple and clear enough. Let's run it:
ng serve
and see the output:
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 in our main directive.
Listen for mousedown event on handle-element. This will work as drag-start trigger.
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.
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.
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.
But, there is still one problem with it. It is allowing user to move element outside the view:
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:
Add an @Input to set custom boundary-element query. By default, we will keep it at body.
Check if we can get the boundary-element using querySelector, if not throw error.
Use boundary-element's layout height and width to adjust the position of dragged element.
// src/app/shared/free-dragging.directive.ts...@Directive({selector:"[appFreeDragging]",})exportclassFreeDraggingDirectiveimplementsAfterViewInit,OnDestroy{...// 1 AddedprivatereadonlyDEFAULT_DRAGGING_BOUNDARY_QUERY="body";@Input()boundaryQuery=this.DEFAULT_DRAGGING_BOUNDARY_QUERY;draggingBoundaryElement:HTMLElement|HTMLBodyElement;...// 2 ModifiedngAfterViewInit():void{this.draggingBoundaryElement=(this.documentasDocument).querySelector(this.boundaryQuery);if(!this.draggingBoundaryElement){thrownewError("Couldn't find any element with query: "+this.boundaryQuery);}else{this.element=this.elementRef.nativeElementasHTMLElement;this.handleElement=this.handle?.elementRef?.nativeElement||this.element;this.initDrag();}}initDrag():void{...// 3 Min and max boundariesconstminBoundX=this.draggingBoundaryElement.offsetLeft;constminBoundY=this.draggingBoundaryElement.offsetTop;constmaxBoundX=minBoundX+this.draggingBoundaryElement.offsetWidth-this.element.offsetWidth;constmaxBoundY=minBoundY+this.draggingBoundaryElement.offsetHeight-this.element.offsetHeight;constdragStartSub=dragStart$.subscribe((event:MouseEvent)=>{...dragSub=drag$.subscribe((event:MouseEvent)=>{event.preventDefault();constx=event.clientX-initialX;consty=event.clientY-initialY;// 4 Update position relativelycurrentX=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)";});});constdragEndSub=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.
//src/styles.csshtml,body{height:100%;}
Let's see the output now:
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:
Locking axes - allow user to drag only in horizontal or vertical direction
Events - generate events for each action, like drag-start, dragging and drag-end
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.
Create a directive in Angular that will allow us to freely drag any element, without using any 3rd party libraries.
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.