Drag & Drop for Lightning Components

Written by srodriki | Published 2017/06/12
Tech Story Tags: salesforce | salesforce-lightning | javascript | sfdc-lightning | lightning-components

TLDRvia the TL;DR App

A Long time ago…

A co-worker and I were talking component-based applications; I was sharing with him some of my lovely experiences with AngularJS (yep, the “1” version) and Vue. It was November and it was getting warm, summer was just around the corner and that by itself made me happy. Yet, my friend was feeling cold and dispirited; he had been working for the past few months in a Salesforce Lightning Application based on the Lightning Components Framework and he was struggling heavily with stuff that in any other framework is just natural. He was having issues with making Drag & Drop work for Lightning Components and was at a dead end… the thing refused to work and he had Lightning to blame.

At that time and after hearing his pledge I just walked away and continued my path, leaving him with nothing more than a simple “I’m sorry for you” (aka. “I think this is your problem and there’s no way in hell I’m getting my hands into it”).

But as usual, the world turns around and around, sometimes leaving you right in front of the problems you once chose to ignore and evade. That much is true for life in general and so is for programming. Few months later I was stuck with the same: Drag & Drop for Lightning Components.

Setting our goal!

I thought it made sense to setup a small project as proof of concept, also acting as a mean to isolate the D&D problem.

The idea is to take the typical column-based layout of applications such as Trello and other Kanban-style apps out there, create some “cards” or items and be able to move them around from column to column.

This is what we’ll need:

  1. A lightning application and a container lightning component for our demo.
  2. Each column or pipeline will be represented as a component, and will all be valid drop targets for our draggable items
  3. Draggable items inside each column will looks like cards, and we’ll be able to move them around at will using the mouse.

Simple, isn’t it?

Ready…

First off I defined a very simple app and main component. Its structure is as follows:

<!-- DragDropContainer.cmp -->aura:component<!-- resources --><ltng:require styles="{!$Resource.bulma + '/bulma-0.4.2/css/bulma.css'}" />

<!-- attributes --><aura:attribute name="allItems" type="list"></aura:attribute>

<!-- event handlers --><aura:handler name="init" action="{!c.doInit}" value="{!this}"></aura:handler><aura:handler name="pipelineChange" event="c:pipelineChange" action="{!c.onPipelineChanged}"></aura:handler>

<div class="columns"><c:pipeline title="New" items="{!v.allItems}"></c:pipeline><c:pipeline title="Closed" items="{!v.allItems}"></c:pipeline><c:pipeline title="Deleted" items="{!v.allItems}"></c:pipeline></div></aura:component>

As easy as it can get, we have 3 “pipelines” holding to hold our draggable items, generated in our component’s “onInit” method, and a handler for a custom event called “pipelineChange”. This event has the following definition:

<!-- pipelineChange.evt --><aura:event type="COMPONENT" description="Event fired whenever an item is dropped on a pipeline"><aura:attribute type="String" name="title"></aura:attribute><aura:attribute type="Object" name="item"></aura:attribute></aura:event>

Here is our controller, showing how we create the items and handle the custom event

<!-- DragDropContainerController.js -->({doInit: function(component, event, helper) {var newItems = [{title: "One item",id: "23243342",status: "New"}];component.set("v.allItems", newItems);},

onPipelineChanged: function(component, event, helper) {var title = event.getParam("title");var item = event.getParam("item");var allLists = component.get("v.allItems");var actualItem = allLists.find(function(el) {return el.id == item.id;});if (actualItem) {actualItem.status = title;component.set("v.allItems", allLists);} else {console.log("could not find item ", item, " in list ", allLists);}}});

So, basically we create a list of items, each with a “status” matching each pipeline (column) we have in our component. For handling the pipelineChanged event we defined a handler which sets the item’s status to the name of the column it’s dropped into and refreshes our collection. I know it’s not optimal, but it will suffice for now.

Set…

Each item will be represented by a “Card” component as follows:

<!-- Card.cmp --><aura:component ><aura:attribute name="item" type="Object" required="true"></aura:attribute><div class="card draggable" draggable="true" ondragstart="{!c.onDragStart}"><div class="card-content">{!v.item.title}</div></div></aura:component>

We declare our component’s main tag as “draggable” so that the browser knows we want users to be able to drag it, and we have a handler for the dragStart event. As you might’ve guessed, we’re going to put our item (attribute for our component) into D&D’s dataTransfer event-set attribute:

<!-- CardController.js -->({onDragStart : function(component, event, helper) {event.dataTransfer.dropEffect = "move";var item = component.get('v.item');event.dataTransfer.setData('text', JSON.stringify(item));}})

Great! We are able to put our whole item, serialized as a JSON string, into the dataTransfer attribute for the D&D event-set (for more information about this, please see the HTML Drag and Drop API). Now, we only need to make sure our “pipeline” component is able to handle when the user drops the objects and we’re done.

And… What?

Usually for the drop event to work on a given HTML element we just need to implement a simple handler function for the ondrop event that receives the event and invokes the `event.preventDefault()`. So, I created the following component for our pipeline

<!-- pipeline.cmp --><aura:component ><!-- Attributes --><aura:attribute name="title" type="String" required="true"></aura:attribute><aura:attribute name="items" type="Object[]" required="true"></aura:attribute>

<!-- Custom Events -->  
<aura:registerEvent name="pipelineChange" type="c:pipelineChange"/>  
  
<div class="column" ondrop="{!c.onDrop}">  
    <div class="panel">  
        <div class="panel-heading">  
            {! v.title }  
        </div>  
        <div class="panel-block column">  
            <aura:iteration items="{!v.items}" var="item">  
                <aura:if isTrue="{!item.status == v.title}">  
                 <c:Card item="{!item}"></c:Card>  
                </aura:if>      
            </aura:iteration>  
        </div>  
    </div>  
</div>  

</aura:component>

Notice I registered the before-mentioned “pipelineChange” event so I can fire it from this component (after all, we want out main component to notice when a user drops a card in one of the pipelines. And this is our controller:

<!-- pipelineController.js -->({onDrop: function(component, event, helper) {event.preventDefault();var pipelineChangeEvent = component.getEvent('pipelineChange');pipelineChangeEvent.setParams({'title': component.get('v.title'),'item': JSON.parse(event.dataTransfer.getData('text'))});pipelineChangeEvent.fire();}})

Nothing new under the sun so far. I handled the ondrop event with a function that ultimately fires the pipelineChange event so that our main component can operate its magic and refresh our lists (again, all this round trip is because I wanted to be more straightforward and not overcomplicate the implementation. It should work, right?

Well, the case is this does not work.

Bad design again, Lightning?

It’s not new some parts of the Lightning Components framework are not very well thought of, plus their execution is even worse. How far does the rabbit hole go is something we discover day by day when developing lightning applications and components so it’s surprising Salesforce messed up native javascript events.

It’s known lightning overrides the “touch” events, replacing them with “click” for touch-based platforms, which is not that bad and even makes sense if you think about it. However, what has been done with the D&D events is a different story.

Apparently, some of the events just don’t work such as “drag” and “dragend”. Why is this, I don’t know. To make things even worse, _some of the events are rendered unable to respect their respective contracts. O_ne such example is the “drop” which loses its key dataTransfer attribute’s values if not handled in a very specific way.

In case you didn’t notice the dataTransfer attribute is key for the whole Drag and Drop mechanism to work. If we can’t get whatever data we stored in previous steps of the chain, we can’t possibly hope to handle a drop event successfully… That means we’re out of business.

Chasing the dragon

I then googled my way through a mist of misleading stuff, limited use cases and even libraries overriding stuff that I just refused on adding for something so simple and basic as handling a Drag & Drop use case.

Probably like Dr. Jekyll did in the popular story, I started mixing potions and recipes for Drag & Drop implementations that I knew would work, even creating a proof of concept of this built on VueJS. Nothing worked and I was about to give up when…

I decided I would preventDefault on all Drag & Drop events I wasn’t using just because and guess what? The thing worked!

I will save you some round trips and tell you the only other event I actually needed to handle with a preventDefault was the dragover event. Why is that? I still do not know.

Finally, our component for the pipeline would look like this:

<!-- pipeline.cmp --><aura:component ><!-- Attributes --><aura:attribute name="title" type="String" required="true"></aura:attribute><aura:attribute name="items" type="Object[]" required="true"></aura:attribute>

<!-- Custom Events -->  
<aura:registerEvent name="pipelineChange" type="c:pipelineChange"/>  
  
<div class="column" ondrop="{!c.onDrop}" ondragover="{!c.allowDrop}">  
    <div class="panel">  
        <div class="panel-heading">  
            {! v.title }  
        </div>  
        <div class="panel-block column">  
            <aura:iteration items="{!v.items}" var="item">  
                <aura:if isTrue="{!item.status == v.title}">  
                 <c:Card item="{!item}"></c:Card>  
                </aura:if>      
            </aura:iteration>  
        </div>  
    </div>  
</div>  

</aura:component>

And this be our controller, which now allows our compoent to work as expected.

<!-- pipelineController.js -->({allowDrop: function(component, event, helper) {event.preventDefault();},

onDrop: function(component, event, helper) {  
    event.preventDefault();  
    var pipelineChangeEvent = component.getEvent('pipelineChange');  
    pipelineChangeEvent.setParams({  
        'title': component.get('v.title'),  
        'item': JSON.parse(event.dataTransfer.getData('text'))  
    });  
    pipelineChangeEvent.fire();  
      
},  

})

And the journey goes on

I hope this rather long article helped some people understand how to handle Drag & Drop features for Lightning Components successfully, but I also hope it depicts the struggle that more often than not the Lightning Components framework complicates stuff instead of being a means to facilitate building rich web applications on the Force.com platform.

You can find the whole repo for this project in Github

Many other examples show us the bad design decisions in Lightning, which most of the time makes me think if we would have been way happier with a framework that would simply provide the very useful $A library and a simple way to call AuraEnabled methods exposed by our Apex controllers from any Javascript, leaving any front-end work to libraries with proven proficiency such as Vue, Angular, React, Inferno, Ember and so forth.

Anyways, while the above does not happen, I’ll (hopefully )be bringing useful solutions to common issues with the Lightning Components framework as soon as I discover them.

Cheers and happy coding!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!


Published by HackerNoon on 2017/06/12