Motivation behind this
I am exploring Lightning Web Components and thought of preparing an use case on drag and drop functionality, but want to leverage native HTML drag-n-drop without using any 3rd party libraries. Also wanted that, this functionality will be build up using two unrelated components and where this drag-n-drop could be challenging.
This motivates to me to build a proof-of-concept and share the challenges and knowledge to community members.
Let's get started.
Use Case
Business has a requirement to see a Company's location in the Google Map when Company info is dragged and dropped to map.
Developer wants to build using Lightning Web Components without 3rd party libraries and want to leverage native HTML functionalities.
Possible End Results
After building the use case, it will perform the functionality as following video:
Solution Approach
Let's first separate the functionalities in the two components as shown in the picture.
displayAccount component will have functionality shown on left side and showAccountLocation component will have functionality as right side.
Flow Diagram
Create a flow diagram like this way to understand the functionalities, flow of control and finalize design approach.
Before going through this below section, it will be better to go through my post on Salesforce Lightning Web Components Cheat Sheet to have overall idea for all types of properties, methods, event propagation etc.
Pre-requisite:
Add pubsub component from repo and add it to project folder. This will be used for event communication using publish-subscribe pattern.
displayAccount component:
displayAccount.html will prepare a screen like this:
Code is as follows:
<template> <lightning-card title="Lightning Web Components: Drag and Drag functionality with pub-sub pattern event communication" icon-name="custom:custom30"> <div class="c-container"> <lightning-layout> <lightning-layout-item padding="around-small"> <template if:true={accounts.data}> <template for:each={accounts.data} for:item="account"> <!-- use draggable="true" to make div as draggable--> <div class="column" draggable="true" key={account.Id} data-item={account.Id} ondragstart={handleDragStart}> <div class="slds-text-heading_small">{account.Name}</div> </div> </template> </template> </lightning-layout-item> </lightning-layout> </div> </lightning-card> </template>
To make <div/> as draggable, draggable="true" and ondragstart={handleDragStart} event handler have been used.
displayAccount.js
This is most important for implementing this functionality and if we follow the flow diagram it is easy to understand which methods are performing which responsibilities. Some important points to be highlighted:
- import LightningElement, wire which will be used in this class.
- import CurrentPageReference and getAccountLocations
- import fireEvent from c/pubsub
- @wire to function i.e CurrentPageReference and getAccountLocations (to retrieve account from database through Apex method)
- register dragover event pragmatically and attached it to template.
this.template.addEventListener('dragover', this.handleDragOver.bind(this));
- handleDragStart method, retrieve AccountId from the div, this is important line.
accountId = event.target.dataset.item;
- After that, retrieve Account record and fire event to the subscribers
fireEvent(this.pageRef, 'selectedAccountDrop', selectedAccount);
Entire js file as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /* eslint-disable no-console */ import { LightningElement, wire} from 'lwc'; import { CurrentPageReference } from 'lightning/navigation'; import { fireEvent } from 'c/pubsub'; import getAccountLocations from '@salesforce/apex/AccountHelper.getAccountLocations'; let i=0; let accountId; //store id from the div let selectedAccount; //store selected account export default class DisplayAccount extends LightningElement { //get page reference @wire(CurrentPageReference) pageRef; constructor() { super(); //register dragover event to the template this.template.addEventListener('dragover', this.handleDragOver.bind(this)); } //retrieve account records from database via Apex class @wire(getAccountLocations) accounts; //when drag is start this method fires handleDragStart(event) { event.dataTransfer.dropEffect = 'move'; //retrieve AccountId from div accountId = event.target.dataset.item; //console.log('event.target.dataset.item=' + event.target.dataset.item); //loop the array, match the AccountId and retrieve the account record for(i=0; i<this.accounts.data.length; i++) { if(accountId!==null && accountId === this.accounts.data[i].Id){ selectedAccount = this.accounts.data[i]; } } //fire an event to the subscribers fireEvent(this.pageRef, 'selectedAccountDrop', selectedAccount); } handleDragOver(event){ event.dataTransfer.dropEffect = 'move'; event.preventDefault(); } } |
AccountHelper.cls
getAccountLocations method it fetches Account data based on SOQL query. I want to show specific records for demo, that's why filter condition has been added.
public with sharing class AccountHelper { @AuraEnabled (cacheable=true) public static List<Account> getAccountLocations(){ return[SELECT Id, Name, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account WHERE isDisplay__c = true]; } }
Let's move to other component.
showAccountLocation component
showAccountLocation.html will end up like this:
Code is as follows:
<template> <lightning-card title="Lightning Map component will show dropped Company Location" icon-name="custom:custom30"> <div class="c-container"> <lightning-layout> <lightning-layout-item padding="around-small"> <!-- define dropzone in the div when item will be dropped--> <div class="dropcls" dropzone="link"> <div class="slds-text-heading_small">{DroppedAccountName}</div> <lightning-map map-markers={mapMarkers} markers-title={DroppedAccountName} zoom-level ={zoomLevel} center ={center}> </lightning-map> <p><lightning-formatted-text value="|--Drop the dragged account here --|" > </lightning-formatted-text></p> </div> </lightning-layout-item> </lightning-layout> </div> </lightning-card> </template>
Important things to note:
- Need to specify dropzone="link" in the div as attribute (pure HTML attribute)
- Use lightning-map to show Account location in Google Map
showAccountLocation.js
This is most important for implementing this functionality and if we follow the flow diagram it is easy to understand which methods are performing which responsibilities. Some important points to be highlighted:
- import registerListener, unregisterAllListeners from c/pubsub
- registering event listeners for dragover and drop events only in the constructor.
this.template.addEventListener('dragover', this.handleDragOver.bind(this)); this.template.addEventListener('drop', this.handleDrop.bind(this));
- connectedCallback and disconnectedCallback methods for registering and unregistering listeners.
- handleSelectedAccountDrop method to capture parameters passed during listening an event raised from displayAccount component.
- handleDrop method prepares the map retrieving values from AccountInfo
- Be sure to use event.preventDefault() wherever necessary.
Entire js file as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | /* eslint-disable no-console */ import { LightningElement, wire, track } from 'lwc'; import { CurrentPageReference } from 'lightning/navigation'; import { registerListener, unregisterAllListeners } from 'c/pubsub'; let acct; export default class ShowAccountLocation extends LightningElement { @track mapMarkers = []; //holds account location related attributes @track markersTitle = ''; //title of markers used in lightning map. @track zoomLevel = 4; //initialize zoom level @track center; //location will be displayed in the center of the map @wire(CurrentPageReference) pageRef; accountInfo; @track DroppedAccountName = ''; constructor() { super(); //these are two must have events to be listended this.template.addEventListener('dragover', this.handleDragOver.bind(this)); this.template.addEventListener('drop', this.handleDrop.bind(this)); } connectedCallback() { // subscribe to selectedAccountDrop event registerListener('selectedAccountDrop', this.handleSelectedAccountDrop, this); } disconnectedCallback() { // unsubscribe from selectedAccountDrop event unregisterAllListeners(this); } //method is called due to listening selectedAccountDrop event handleSelectedAccountDrop(accountInfo) { this.accountInfo = accountInfo; } //when item is dropped this event fires handleDrop(event){ console.log('inside handle drop'); if(event.stopPropagation){ event.stopPropagation(); } event.preventDefault(); //assigns account records acct = this.accountInfo; this.DroppedAccountName = acct.Name; //prepares information for the lightning map attribute values. this.markersTitle = acct.Name; this.mapMarkers = [ { location: { Street: acct.BillingStreet, City: acct.BillingCity, State: acct.BillingState, Country: acct.BillingCountry, }, icon: 'custom:custom26', title: acct.Name, } ]; this.center = { location: { City: acct.BillingCity, }, }; this.zoomLevel = 6; } //when item is dragged on the component this event fires handleDragOver(event){ event.dataTransfer.dropEffect = 'move'; event.preventDefault(); } } |
meta files should include where these components are available.
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="displayAccount"> <apiVersion>46.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__RecordPage</target> <target>lightning__AppPage</target> <target>lightning__HomePage</target> </targets> </LightningComponentBundle>
I have used .css file googling on the net as I am not good in styling.
Create a Lightning App Builder page with two regions and place those components and run the application, it will display the page like this.
Finally we are done and thanks for reading.