Drag and Drop in LWC
This post will run through the simple way that got me understanding better the Drag and Drop API.
I will share an example below of how to integrate a drag and drop functionality into your lightning web component.
When I first dealing with Drag and Drop it became very clunky and confusing for me, so I’ve tried to put some sense in simple example.
I’ve looked for the Salesforce way of implementing the styling elements which seems suitable for my needs but obviously I quickly seen that to create a slick experience I typically add few more styles of my own.
In todays web development it is a very easy task to add D&D functionality with the standard API.
For attaching an item to be draggable we simply need the following:
draggable="true"
Then you handle the event lifecycle via your scripts.
Let’s run through the events available:
Events fired on the draggable target:
ondragstart
– the user starts dragging an item.ondrag
– a dragged item (element or text selection) is dragged.ondragend
– a drag operation ends (such as releasing a mouse button or hitting the Esc key;
Events fired on the drop target:
ondragenter
– a dragged item enters a valid drop targetondragover
– a dragged item is being dragged over a valid drop target, every few hundred milliseconds.ondragleave
– a dragged item leaves a valid drop target.ondrop
– an item is dropped on a valid drop target.
Break down steps
- We have a DOM element set with
draggable="true"
- When a user select an element (
onmousedown
) and start dragging(ondragstart)
the Draggable Dom element we start our Drag & Drop event lifecycle.- While dragging an element, the
ondrag
event fires every 350 milliseconds. - Its less recommended to use this event as it’s not so accurate due to the time interval, but it is available.
- While dragging an element, the
- This cycle on the dragging element will end with
ondragend
event – This is where you will typically Save the changes done on this draggable element or data.
In Same time we invoke the events on any droppable area element, which by the way, may be same as the draggable element.
- While the dragging element is being dragged, The User will try to locate a valid drop target.
- We control what goes inside an element by using :
ondragenter
– This event is called once our mouse is entered a potential drop zone.- OR/And using
ondragover
– will be invoked once our draggable element is completely hovered over the droppable element.
- We control what goes inside an element by using :
- In case you stepping out of the droppable element –
ondragleave
will be invoked – This helps if you need to undo changes done to UI when entered. - Once dropped on the droppable element –
ondrop
will be fired – Thats the last event fired on the Droppable area.
Once drag being started and typically once ended – when using our mouse or touch pad – different events will also fire like mouse events and touch events.
Those also can handle some dragging features by themselves.
Mouse Events :
- onmousedown
- onmouseup
Touch support :
- ontouchstart
- ontouchover
- ontouchmove
- ontouchend
- ontouchcancel
But in this article we mainly focus on the pure Drag and Drop API events.
Lets explorer how Salesforce lightning design system (SLDS) allow to style the draggable and droppable elements when firing the events.
Handle the Data Transfer
The dataTransfer
object can store data from the draggable element and pass it through the event lifecycle. I may use us as validation and to tell the browser there is a Drag and drop event in process.
// On drag starts... onTileDragStart(event) { // Set the draggable element index event.dataTransfer.setData('text/index', event.target.dataset.index ); } // On Drop of dragable item on droppable element onTileDrop(event){ // Get draggable data index to validate with draggableElement assigned const draggableDataIndex = event.dataTransfer.getData("text/index"); } // Dragging operation ends... onTileDragEnd(event) { // Clear Drag and Drop Data event.dataTransfer.clearData("text/index"); }
Apply Styling for each Step:
I’ve been very confused at first from all those different events and which one to use to apply which styling class.
So I tried to break it down in this simplified diagram.
event.preventDefault();
and event.stopPropagation();
to prevent the default behaviour which is to drag and drop Text/Links from being active.
Lets run through some popular styling for draggable and droppable elements which can be applied in any event fired.
- cursor – grab/grabbing, move, no-drop, copy : those are few relevant cursor to replace the regular pointer and will show more clearly the ability to drag and move the element.
- transform – twist and animate the element.
- border – highlighting the draggable element with certain colours and styles help to differentiate it from others.
- opacity – transparent elements gives the illusion of inactive elements.
From SLDS we get slds-is-draggable
and slds-is-grabbed
.
Lets Jump into example where we implement the app luncher styling with draggable element.
<template> <!-- APP LUNCHER --> <ul class="slds-has-dividers_bottom-space tile-actions-list draggable-elements" > <template for:each={records} for:item="record" for:index="idx"> <li key={record.id} data-index={idx} data-record={record.id} class="slds-p-horizontal_small slds-size_1-of-1" > <div draggable="true" class="slds-app-launcher__tile slds-text-link_reset slds-is-draggable" ondragstart={onTileDragStart} ondragend={onTileDragEnd} ondragenter={onTileDragEnter} ondragleave={onTileLeave} ondragover={onTileDragOver} ondrop={onTileDrop} > <div class="slds-app-launcher__tile-figure"> <lightning-avatar class="slds-avatar-group_large slds-align_absolute-center" fallback-icon-name={record.icon} size="medium" alternative-text={record.name}></lightning-avatar> <div class="slds-m-top_xxx-small"> <lightning-button-icon class="slds-p-horizontal_x-small" aria-hidden="true" aria-pressed="false" variant="bare" name="reorder" title="Reorder" icon-name="utility:rows" ></lightning-button-icon> </div> </div> <div class="slds-app-launcher__tile-body"> <a href="javascript:void(0);" class="slds-text-heading_small" onclick={onTileSelect}>{record.name} </a> <p>{record.description}</p> </div> </div> </li> </template> </ul> </template>
We apply draggable="true"
to our list item element and adding our relevant listeners for each event.
import { LightningElement, track } from 'lwc'; export default class SimpleDraggable extends LightningElement { @track records; connectedCallback(){ // sets the records data this.records = this.fetchRecords(); } fetchRecords() { let items = []; for(let i = 1; i < 30; i++) { items.push({ id: i, name: `item ${i}`, icon: `custom:custom${i}`, description: `some description for item ${i}...` }); } return items; } // Drag and Drop draggableElement; droppableElement; // Draggable Events // the user starts dragging an item. onTileDragStart(event) { const currentElement = event.target; const listItemElement = currentElement.closest('LI'); // Assign draggableElement this.draggableElement = listItemElement; // Add styling to current Dragged element const divElement = currentElement.closest('.slds-is-draggable'); divElement.classList.add("slds-is-grabbed"); console.log('setData '+ this.draggableElement.dataset.record); // Drag and drop data event.dataTransfer.setData('text/index', this.draggableElement.dataset.record); event.dataTransfer.dropEffect = "move"; } // a drag operation ends onTileDragEnd(event) { // Turn off all draggable styling const draggableItems = this.template.querySelectorAll('.slds-is-draggable'); draggableItems.forEach(element => { if(element.classList.contains("slds-is-grabbed")) { // removed grabbed styling element.classList.remove("slds-is-grabbed"); } }); // Clear Drag and Drop Data event.dataTransfer.clearData("text/index"); } // Droppable Events // a dragged item enters a valid drop target onTileDragEnter(event){ // Prevent additional drop after dropped and allow the ondrop to work event.preventDefault(); event.stopPropagation(); const listItemElement = event.target.closest('LI'); const divElement = event.target.closest('.slds-is-draggable'); if(!divElement.target.classList.contains("slds-is-grabbed")) { divElement.classList.add("slds-is-grabbed"); // // Assign to Target element this.droppableElement = listItemElement; } } // a dragged item leaves a valid drop target. onTileLeave(event) { const currentElement = event.target; const divElement = currentElement.closest('.slds-is-draggable'); // Remove styling from list item that was hovered on if(divElement.classList.contains("slds-is-grabbed")) { divElement.classList.remove("slds-is-grabbed"); } } // a dragged item is being dragged over a valid drop target, every few hundred milliseconds. onTileDragOver(event) { // Prevent additional drop after dropped and allow the ondrop to work event.preventDefault(); event.stopPropagation(); const currentElement = event.target; const listItemElement = currentElement.closest('LI'); const divElement = currentElement.closest('.slds-is-draggable'); if(!divElement.classList.contains("slds-is-grabbed")) { divElement.classList.add("slds-is-grabbed"); // Re-Assign Target Element this.droppableElement = listItemElement; } // on dragover consider swap the elements on UI before server //this.swapElements(); } onTileDrop(event){ // Get draggable data index to validate with draggableElement assigned const draggableDataIndex = event.dataTransfer.getData("text/index"); const validateDraggableData = draggableDataIndex === this.draggableElement.dataset.record; const isSameRecord = this.droppableElement.dataset.record === this.draggableElement.dataset.record === validateDraggableData; if(!isSameRecord) { // Swap and save elements when tile dropped this.swapElements(this.draggableElement, this.droppableElement); } } // swap the draggable and droppable on UI swapElements(draggableElement, droppableElement) { const sourceIndex = draggableElement.dataset.index; const targetIndex = droppableElement.dataset.index; // SWAP Elements on UI [ this.records[sourceIndex], this.records[targetIndex] ] = [ this.records[targetIndex], this.records[sourceIndex]]; } // on select item onTileSelect(event) { event.preventDefault(); event.stopPropagation(); // do something on select } }
There are many options to play with the styling and what happens in each event – this is hopefully a very simple example just to get you started.
Resources: