Salesforce LWC: Basic Drag and Drop Functionality

Recently I attended Salesforce VirtualDearmin session and I found this session very interesting and I thought to share this with everyone of you. For more detail you can refer below link on Drag and Drop Functionality:
https://forcementor.github.io/lwc_dnd_20/#/

In this blog we will see about Drag and Drop functionality in Salesforce using Lightning Web Component.
To implement drag and drop functionality there are few standard HTML events which we will use to implement.
Lets see first what we are going to achieve in this blog with a small video-


Now we have clear understanding what we are going to achieve. Let us go to the code.

First we will create very basic UI with 3 layout-item. One for each New Task, In Progress Task and Completed Task.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
    <lightning-layout vertical-align="stretch" multiple-rows="true" class="x-large">
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">New Task</div>
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">In Progress Task</div>
            
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">Completed Task</div>
            
        </lightning-layout-item>
    </lightning-layout>
</template>

Now we will add some UI login in first layout-item:

 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
<template>
    <lightning-layout vertical-align="stretch" multiple-rows="true" class="x-large">
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-box slds-p-around_medium slds-text-align_center">
                <div class="slds-text-heading_large">New Task</div>
                <template if:true={taskNewList}>
                    <template for:each={taskNewList} for:item="data">
                        <div key={data.Id} draggable="true" id={data.Id} data-id={data.Id} ondragstart={taskDragStart} ondragend={taskDragEnd} class="slds-p-around_small">
                            <lightning-card  variant="Narrow" title={data.Subject} icon-name="standard:today">
                                <div class="slds-p-horizontal_small">
                                    <template if:true={data.ContactName}>
                                        Contact Name - <lightning-formatted-url value={data.WhoId} label={data.ContactName} target="_blank" ></lightning-formatted-url><br/><br/>
                                    </template>
                                    <template if:true={data.AccountName}>
                                        Account Name - <lightning-formatted-url value={data.WhatId} label={data.AccountName} target="_blank" ></lightning-formatted-url><br/>
                                    </template>
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </template>
            </div>
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">In Progress Task</div>
            
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">Completed Task</div>
            
        </lightning-layout-item>
    </lightning-layout>
</template>

In above code if you see we have just iterating over taskNewList variable which holds all Task which are not In Progress or Completed.
The only additional code is highlighted above which is a div tag with attribute draggable which makes lightning-card draggable. So in that div tag we have used few HTML standard events which help us to implement drag and drop functionality:

1. ondragstart: This event will fire when we will start dragging element under the div tag.
2. ondragend: This event will fire when we will end dragging element under the div tag.

Once above two event will fire then below Java Script will execute. In taskDragStart method we are getting the Task Id which we started to drag and updating the CSS of selected lightning-card.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
taskDragStart(event){
        const taskId = event.target.id.substr(0,18);
        //window.alert(taskId);
        this.dropTaskId = taskId;
        let draggableElement = this.template.querySelector('[data-id="' + taskId + '"]');
        draggableElement.classList.add('drag');
        this.handleTaskDrag(taskId);
    }

    taskDragEnd(event){
        const taskId = event.target.id.substr(0,18);
        //window.alert(taskId);
        let draggableElement = this.template.querySelector('[data-id="' + taskId + '"]');
        draggableElement.classList.remove('drag');
    }

Below is the CSS which will apply once we will start dragging lightning-card:

1
2
3
4
.drag {
    border-style: solid dotted;
    opacity: 0.20;
}

Now lets take a look into the second layout-item where we will drop the selected lightning-card element:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">In Progress Task</div>
            <div id="InProgress" style="height:100%" data-role="drop-target" ondrop={handleDrop} ondragenter={handleDragEnter} ondragover={handleDragOver} ondragleave={handleDragLeave}>
                <template if:true={taskInProgressList}>
                    <template for:each={taskInProgressList} for:item="data">
                        <div key={data.Id} draggable="true" id={data.Id} data-id={data.Id} ondragstart={taskDragStart} ondragend={taskDragEnd} class="slds-p-around_small">
                            <lightning-card style="width: 50px;"  variant="Narrow" title={data.Subject} icon-name="custom:custom18">
                                <div class="slds-p-horizontal_small">
                                    <template if:true={data.ContactName}>
                                        Contact Name - <lightning-formatted-url value={data.WhoId} label={data.ContactName} target="_blank" ></lightning-formatted-url><br/><br/>
                                    </template>
                                    <template if:true={data.AccountName}>
                                        Account Name - <lightning-formatted-url value={data.WhatId} label={data.AccountName} target="_blank" ></lightning-formatted-url><br/>
                                    </template>
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </template>
            </div>
        </lightning-layout-item>

If you see above highlighted div tag it is having few HTML standard events which fires when any element enter into the div:

1. ondrop: When element is dropped under div tag then this event will fire.
2. ondragenter: When element is enter the div tag then this event will fire.
3. ondragover: When element is over the div tag then this event will fire.
4. ondragleave: When element leaves div tag then this event will fire.

 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
handleDrop(event){
        this.cancel(event);
        const columnUsed = event.target.id;
        let taskNewStatus;
        if(columnUsed.includes('InProgress')){
            taskNewStatus = 'In Progress';
        }else if(columnUsed.includes('newTask')){
            taskNewStatus = 'Not Started';
        }else if(columnUsed.includes('completed')){
            taskNewStatus = 'Completed';
        }
        //window.alert(columnUsed + ' & '+ taskNewStatus);
        this.updateTaskStatus(this.dropTaskId, taskNewStatus);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.remove('over');
    }

    handleDragEnter(event){
        this.cancel(event);
    }

    handleDragOver(event){
        this.cancel(event);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.add('over');
    }

    handleDragLeave(event){
        this.cancel(event);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.remove('over');
    }

handleDrop is getting called when we are dropping element under Div tag. After dropping we are calling handleDrop which is checking the layout-item under which element is dropped. If it is dropped under In progress Task then task status needs to be updated.

Other 3 events are handling the CSS of layout-item based on event.

Below is the complete logic of this functionality:

 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
<template>
    <lightning-layout vertical-align="stretch" multiple-rows="true" class="x-large">
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-box slds-p-around_medium slds-text-align_center">
                <div class="slds-text-heading_large">New Task</div>
                <template if:true={taskNewList}>
                    <template for:each={taskNewList} for:item="data">
                        <div key={data.Id} draggable="true" id={data.Id} data-id={data.Id} ondragstart={taskDragStart} ondragend={taskDragEnd} class="slds-p-around_small">
                            <lightning-card  variant="Narrow" title={data.Subject} icon-name="standard:today">
                                <div class="slds-p-horizontal_small">
                                    <template if:true={data.ContactName}>
                                        Contact Name - <lightning-formatted-url value={data.WhoId} label={data.ContactName} target="_blank" ></lightning-formatted-url><br/><br/>
                                    </template>
                                    <template if:true={data.AccountName}>
                                        Account Name - <lightning-formatted-url value={data.WhatId} label={data.AccountName} target="_blank" ></lightning-formatted-url><br/>
                                    </template>
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </template>
            </div>
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">In Progress Task</div>
            <div id="InProgress" style="height:100%" data-role="drop-target" ondrop={handleDrop} ondragenter={handleDragEnter} ondragover={handleDragOver} ondragleave={handleDragLeave}>
                <template if:true={taskInProgressList}>
                    <template for:each={taskInProgressList} for:item="data">
                        <div key={data.Id} draggable="true" id={data.Id} data-id={data.Id} ondragstart={taskDragStart} ondragend={taskDragEnd} class="slds-p-around_small">
                            <lightning-card style="width: 50px;"  variant="Narrow" title={data.Subject} icon-name="custom:custom18">
                                <div class="slds-p-horizontal_small">
                                    <template if:true={data.ContactName}>
                                        Contact Name - <lightning-formatted-url value={data.WhoId} label={data.ContactName} target="_blank" ></lightning-formatted-url><br/><br/>
                                    </template>
                                    <template if:true={data.AccountName}>
                                        Account Name - <lightning-formatted-url value={data.WhatId} label={data.AccountName} target="_blank" ></lightning-formatted-url><br/>
                                    </template>
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </template>
            </div>
        </lightning-layout-item>
        <lightning-layout-item flexibility="auto" padding="around-small" class="custom-box">
            <div class="slds-text-heading_large">Completed Task</div>
            <div id="completed" style="height:100%" data-role="drop-target" ondrop={handleDrop} ondragenter={handleDragEnter} ondragover={handleDragOver} ondragleave={handleDragLeave}>
                <template if:true={taskCompletedList}>
                    <template for:each={taskCompletedList} for:item="data">
                        <div key={data.Id} draggable="true" id={data.Id} data-id={data.Id} ondragstart={taskDragStart} ondragend={taskDragEnd} class="slds-p-around_small">
                            <lightning-card style="width: 50px;"  variant="Narrow" title={data.Subject} icon-name="standard:resource_preference">
                                <div class="slds-p-horizontal_small">
                                    <template if:true={data.ContactName}>
                                        Contact Name - <lightning-formatted-url value={data.WhoId} label={data.ContactName} target="_blank" ></lightning-formatted-url><br/><br/>
                                    </template>
                                    <template if:true={data.AccountName}>
                                        Account Name - <lightning-formatted-url value={data.WhatId} label={data.AccountName} target="_blank" ></lightning-formatted-url><br/>
                                    </template>
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </template>
            </div>
        </lightning-layout-item>
    </lightning-layout>
</template>

Java Script Logic:

  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
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { LightningElement, track, wire } from 'lwc';
import taskData from '@salesforce/apex/DragAndDropComponentHandler.getAllTask';
import updateTask from '@salesforce/apex/DragAndDropComponentHandler.updateTask';

export default class DragAndDropComponent extends LightningElement {
    @track taskNewList = [];
    @track taskInProgressList = [];
    @track taskCompletedList = [];
    @track dropTaskId;

    connectedCallback(){
        this.getTaskData();
    }

    getTaskData(){
        taskData().then(result =>{
            let taskNewData = [];
            let taskInProgressData = [];
            let taskCompletedData = [];
            for(let i = 0; i < result.length; i++){
                let task = new Object();
                task.Id = result[i].Id;
                task.Subject = result[i].Subject;
                task.Status = result[i].Status;
                task.Description = result[i].Description;
                task.WhoId = '/'+result[i].WhoId;
                task.WhatId = '/'+result[i].WhatId;
                if(result[i].WhoId !== undefined){
                    task.ContactName = result[i].Who.Name;
                }
                if(result[i].WhatId !== undefined){
                    task.AccountName = result[i].What.Name;
                }
                if(task.Status === 'Not Started'){
                    taskNewData.push(task);
                }else if(task.Status !== 'Not Started' && task.Status !== 'Completed'){
                    taskInProgressData.push(task);
                }else if(task.Status === 'Completed'){
                    taskCompletedData.push(task);
                }
            }
            this.taskNewList = taskNewData;
            this.taskInProgressList = taskInProgressData;
            this.taskCompletedList = taskCompletedData;
        }).catch(error => {
            window.alert('$$$Test1:'+ error);
        })
    }

    taskDragStart(event){
        const taskId = event.target.id.substr(0,18);
        //window.alert(taskId);
        this.dropTaskId = taskId;
        let draggableElement = this.template.querySelector('[data-id="' + taskId + '"]');
        draggableElement.classList.add('drag');
        this.handleTaskDrag(taskId);
    }

    taskDragEnd(event){
        const taskId = event.target.id.substr(0,18);
        //window.alert(taskId);
        let draggableElement = this.template.querySelector('[data-id="' + taskId + '"]');
        draggableElement.classList.remove('drag');
    }

    handleDrop(event){
        this.cancel(event);
        const columnUsed = event.target.id;
        let taskNewStatus;
        if(columnUsed.includes('InProgress')){
            taskNewStatus = 'In Progress';
        }else if(columnUsed.includes('newTask')){
            taskNewStatus = 'Not Started';
        }else if(columnUsed.includes('completed')){
            taskNewStatus = 'Completed';
        }
        //window.alert(columnUsed + ' & '+ taskNewStatus);
        this.updateTaskStatus(this.dropTaskId, taskNewStatus);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.remove('over');
    }

    handleDragEnter(event){
        this.cancel(event);
    }

    handleDragOver(event){
        this.cancel(event);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.add('over');
    }

    handleDragLeave(event){
        this.cancel(event);
        let draggableElement = this.template.querySelector('[data-role="drop-target"]');
        draggableElement.classList.remove('over');
    }

    handleTaskDrag(taskId){
        console.log('$$$TEst: '+ taskId);
    }

    updateTaskStatus(taskId, taskNewStatus){
        updateTask({newTaskId: taskId, newStatus: taskNewStatus}).then(result =>{
            this.getTaskData();
        }).catch(error =>{
            window.alert('$$$Test2:'+ JSON.stringify(error));
        })
    }

    cancel(event) {
        if (event.stopPropagation) event.stopPropagation();
        if (event.preventDefault) event.preventDefault();
        return false;
    };

}

Cascade Style Sheet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.c-container {
    border: 1px solid #d8dde6;
    margin: 10px 0 20px 0;
}
.custom-box {
    text-align: center;
    background-color: #f4f6f9;
    padding: 1rem;
    border: 1px solid #d8dde6;
}
.drag {
    border-style: solid dotted;
    opacity: 0.20;
}
.over{
    border-style: solid dotted;
    border-color: green;
}

Apex Class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public with sharing class DragAndDropComponentHandler {
    @AuraEnabled
    public static List<Task> getAllTask(){
        Id loggedInUserId = UserInfo.getUserId();
        return [Select Id, Owner.Name, Subject, Status, WhoId, WhatId, Who.Name, What.Name, Description from Task where OwnerId = :loggedInUserId];
    }

    @AuraEnabled
    public static void updateTask(Id newTaskId, String newStatus){
        Task updateTask = new Task(Id = newTaskId, Status = newStatus);
        Database.update(updateTask);
    }
}

Code Reference:




References:


Other Useful blogs:

  • https://www.sfprompt.com/
  • https://manish-porwal.blogspot.com/2019/11/draw-chart-in-lwc-using-chartjs.html








Comments

Followers

Popular posts from this blog

Salesforce LWC: Multi-Record Creation using Lightning Web Component

Salesforce Lightning Web Component: Dynamic Column Selection in Lightning Datatable