There are many Drag and Drop, aka. ‘dnd, libraries out there. This article speaks about how to fix the cursor icon for the PrimeNG drag and drop module when you can’t use a different library.
This is a situation I recently faced when working on a Angular + PrimeNG project.
So for this post, we will be fixing PrimeNG drag and drop mouse cursor
What we are making
We are creating a solution to enable drag and drop areas with multiple scopes.
The difficulty with the existing implementation is to disable the drop cursor icon when the scopes don’t match. PrimeNG does not provide support for that.
You can see a working example of the final solution in this link https://k89pc3.csb.app/
What is PrimeNG?
PrimeNG is a collection of rich UI components for Angular. All widgets are open source and free to use under MIT License.
This means that It provides tons of UI components and utilities, and among them, a Drag and Drop one.
How PrimeNG drag and drop works
PrimeNG dnd allows you to easily enable elements to be dragged, and other elements where you can drop on.
It even allows you to define scopes for those elements, meaning that you can’t drop an element of one scope into the drop element of a different scope.
That’s awesome!
Unfortunately, this directives don’t disable the cursor icon. This means that even if you drag an element of scope APPLES over a drop zone with scope ORANGES, you will still see the drop cursor icon.
Instead, we want to show the forbridden icon for this situations:
You can find the official documentation for this component in their website: https://www.primefaces.org/primeng-v14/dragdrop
Why does this happen?
This has to do with how the browsers native API for drag and drop works.
To define a draggable item and a drop zone, we just need to set an event handler on each element:
ondragstart on the draggable element + “draggable” attribute
ondrop & ondragover on the droppable element
From the MDN
Making an element draggable requires adding the
MDNdraggable
attribute and theondragstart
event handler
By default, the browser prevents anything from happening when dropping something onto most HTML elements. To change that behavior so that an element becomes a drop zone or is droppable, the element must have both
MDNondragover
andondrop
event handler attributes.
The problem in PrimeNG drag and drop directvies
If we check the PrimeNG source code for the dragdrop directives, we can see that they create those handlers from the beggining, (in the AfterViewInit() hook) so drop areas ALWAYS have the droppable “status”.
This makes the browser to always show the “drop” cursor icon when dragging anything over that drop area, regardless of the scopes we set to them.
ngAfterViewInit() { if (!this.pDroppableDisabled) { this.bindDragOverListener(); } } bindDragOverListener() { if (!this.dragOverListener) { this.zone.runOutsideAngular(() => { this.dragOverListener = this.dragOver.bind(this); this.el.nativeElement.addEventListener('dragover', this.dragOverListener); }); } }
event.dataTransfer and browser restrictions
The MDN also states we have aproperty called “dataTransfer” that allows us to share data between the drag element and the drop one.
This does not work.
You can only get that event in the “ondrop” event. The reason is:
The data is only available on drop, this is a security feature since a website could grab data when you happen to be dragging something across the webpage.
Musa, via StackOverflow
You can read more on this limitation in the StackOverflow topic
This makes impossible to use the native dataTransfer object to check It’s data in the ondragenter event, and thus we can’t perform any scope check.
The solution
Creating a service to hold drag data
As we are using Angular, there is a quite simple workaround to the dataTransfer object limitation. Using a service to hold our dragged data and scope.
It needs to feature a property to store that data, one method to set It’s value and another one to clear it.
TIP: If you are using angular CLI you can use the following command to generate the service skeleton:
ng g service folder/service-name
For example:
The command: ng g service services/drag-and-drop
Will generate the file: src/app/services/drag-and-drop.service.ts
A minimal implementation may look like the following:
import { Injectable } from "@angular/core"; /** * Interface to hold information on what we are dragging * and what scope (elementType) it has. We allow multiple * scopes */ interface Clipboard { scope: string | string[]; payload: any; } /** * Sample service to hold the information of what we are dragging * and what scopes it has (where can we drop it and where not) */ @Injectable({ providedIn: "root" }) export class DragAndDropService { private clipboard: Clipboard = null; /** * Save element data and scope on the "clipboard" * @param scope scope for this dragged element * @param payload data of the dragged element */ public startDragging(scope: string, payload: any): void { this.clipboard = { scope, payload }; } /** * Check wether the current dragging elements can be dropped * on a target or not. * @param targetScope target scope we want to check for dropping */ public canDrop(targetScope: string): boolean { const draggingScope = this.clipboard?.scope; if (typeof draggingScope === "string") { return draggingScope === targetScope; } else if (Array.isArray(draggingScope)) { return draggingScope.some((scope) => scope === targetScope); } return false; } }
Extending PrimeNG directives to check the scopes
This is just half of the issue. Now we have to customize the draggable and droppable directives to use this service to check if the scopes match before listening for the dragenter event.
What we want to do is to set the service data on the startDrag() method from the draggable, and check if this scope match with the drop element.
If they match, we subscribe to the drop element (droppable) event, what will make the mouse cursor icon to appear properly.
If they don’t match, we won’t subscribe and thus the default browser behavior will show the disabled cursor on the UI.
Draggable directive
As stated in the paragraph above, the draggable directive will override the dragStart() method to do the default behavior ( super.dragStart() ) but also to set the desired data and the scope to the DragAndDropService.
A minimal implementation for the draggable directive is:
import { Directive, ElementRef, Input, NgZone } from "@angular/core"; import { Draggable } from "primeng-lts/dragdrop"; import { DragAndDropService } from "../services/drag-and-drop.service"; /** * NOTE: We are extending Draggable class directly because this Angular version * does not have hostDirectives[] property for directives. For future versions * It would be better to compose this directive with the other and not to extend It. */ @Directive({ selector: "[dndDraggable]" }) export class DndDraggableDirective extends Draggable { @Input("dndDraggable") scope: string; constructor( public el: ElementRef, public zone: NgZone, public dragAndDropService: DragAndDropService ) { super(el, zone); } dragStart(event) { super.dragStart(event); this.dragAndDropService.startDragging(this.scope, { action: "CREATE", props: { data: "Dummy data" } }); } }
Droppable directive
In a similar fashion to the draggable directive, in the droppable one we will retrieve the scope from the service and check whether It matches with the droppable or not.
A minimal implementation for a droppable directive could be:
import { Directive, ElementRef, NgZone, Input, AfterViewInit } from "@angular/core"; import { Droppable } from "primeng-lts/dragdrop"; import { DragAndDropService } from "../services/drag-and-drop.service"; /** * NOTE: We are extending Draggable class directly because this Angular version * does not have hostDirectives[] property for directives. For future versions * It would be better to compose this directive with the other and not to extend It. */ @Directive({ selector: "[dndDroppable]" }) export class DndDroppableDirective extends Droppable implements AfterViewInit { @Input("dndDroppable") scope: string | string[]; // Override pDroppable input to avoid instantiating PrimeNg droppable directive constructor( public el: ElementRef, public zone: NgZone, public dragAndDropService: DragAndDropService ) { super(el, zone); } /** * Override parent initialization to avoid binding dragOver listeners. * If you bind to the listener, you will always get the "drop" mouse icon * regardless of the "allowDrop()" evaluation result. */ ngAfterViewInit() {} // Override parent's init /** * Event on drag enter to enable/disable dropping on this target. * @param event Drag enter event */ dragEnter(event: any) { super.dragEnter(event); if (this.allowDrop()) { this.bindDragOverListener(); } else { this.unbindDragOverListener(); } } /** * Chech wether the dragged object is allowed to be dropped here or not. * Overrides default directive checks */ allowDrop(): boolean { if (typeof this.scope === "string") { return this.dragAndDropService.canDrop(this.scope); } else if (Array.isArray(this.scope)) { return this.scope.some((scope) => this.dragAndDropService.canDrop(scope)); } return false; } }
Full working source code
To sum up, all the above explanations and pieces of code can be tested and download as a full (and minimal) Angular project.
This includes PrimeNG and the service + directives shown earlier in this post.
If you liked this post, please leave a comment so that I can keep up the good work. If you think it can be improved, please let me know how and I will take It into account for future posts!
Don’t forget to signup if you want to stay up to date with my latest posts!
Signup now with Discord!