Lightning Web Components – How to Sort Custom List Items via Drag and Drop

lightning-web-components

Using the great work of Don Robins' presentation on LWC Drag and Drop at https://forcementor.github.io/lwc_dnd_20/#/ I've implemented the majority of a solution that allows a drag and drop enabled version of the classic dual listview approach (move items from list A to list B).

The list items themselves are rendered as rich text, but essentially there's three key LWC components here

  1. reportSectionItem – the individual draggable item
  2. reportSectionList – the list container(s) that act as drag targets
  3. reportSectionPicker – the component housing two lists and handling the events

I have the drag and drop between lists working and updating a backend object

Backend objects, managed by an Apex class ReportSectionPicker, are

  • Report_Section__c – items that are selectable
  • Report_Section_List__c – items that are selected, for a given recordId (where the object in this case is WorkOrder, but that shouldn't be relevant)

The latter object, Report_Section_List__c has a field "Sequence__c" to track the order of selected items (and the Apex class SOQLs them out ordered by that field).

What I'm struggling with is the last hurdle – the ability to sort selected items within the selected list. I've seen examples of drag and drop sorting of table rows in LWC, such as https://lovesalesforceyes.blogspot.com/2020/12/sorting-table-rows-by-drag-and-drop-in.html but I'm not sure how to implement comparable functionality in my component model.

I think I need to track not just the list as a drop target, but track the items within that list it's being dragged over and do some kind of shuffling (both visually in terms of moving visual elements and in the underlying data) based on that. The impact would be not just re-sequencing the dragged element, but potentially the elements surrounding it.

Currently, I don't even know where to start. Architecturally, events are fired from child to parent to be eventually handled by the reportSectionPicker.js code, which follows Don's original design, but I think I need to "enrich" the events being passed with more data to handle this scenario.

Any pointers on how to handle this requirement?

GitHub repo containing the Apex handler, the LWCs, a Flexipage and the objects at https://github.com/klivian/ReportPicker but it's more for showing the implementation than having something immediately runnable

Best Answer

I wrote a very simplified drag-and-drop implementation, derived from my older drag-and-drop component written in Aura. You should not use this code directly in production, it requires some polishing to make it fully functional. However, I hope it successfully demonstrates how to do what you're trying to do in a generic manner.

Container Logic

  availableItems = []
  selectedItems = []
  // Temp storage set on drag start
  dragInfo
  handleDragStart(event) {
    // Keep track of the list and item id
    this.dragInfo = { ...event.detail }
  }
  handleDragComplete(event) {
    // Keep reference to the lists. Start and end lists may be the same list.
    let startList = this.dragInfo.name === 'available'? this.availableItems: this.selectedItems
    let endList = event.detail.name === 'available'? this.availableItems: this.selectedItems
    // Indices for the items to move in their respective lists
    let startIndex = startList.findIndex(item => item.id === this.dragInfo.id)
    let endIndex = endList.findIndex(item => item.id === event.detail.id)
    // Remove from old index, move to new index
    endList.splice(endIndex, 0, startList.splice(startIndex, 1)[0])
    // Trigger a render cycle on copy. You could also use @track.
    this.availableItems = [...this.availableItems]
    this.selectedItems = [...this.selectedItems]
  }

Container Markup

<lightning-layout>
    <lightning-layout-item size="6">
        <c-drag-list-container name="available" items={availableItems} onstartmove={handleDragStart}
            onendmove={handleDragComplete}>
        </c-drag-list-container>
    </lightning-layout-item>
    <lightning-layout-item size="6">
        <c-drag-list-container name="selected" items={selectedItems} onstartmove={handleDragStart}
            onendmove={handleDragComplete}>
        </c-drag-list-container>
    </lightning-layout-item>
</lightning-layout>

List Logic

  @api items
  @api name
  cancel(event) {
    event.preventDefault();
    event.stopPropagation();
  }
  handleDragComplete(event) {
    // Fire custom event when drag stops
    const detail = { name: this.name, id: event.target.dataset?.id }
    console.log(detail)
    this.dispatchEvent(
      new CustomEvent(
        'endmove', 
        { detail }
      )
    )
  }
  // Fire custom event whend drag starts
  handleDragStart(event) {
    const detail = { name: this.name, id: event.target.dataset?.id }
    this.dispatchEvent(
      new CustomEvent(
        'startmove', 
        { detail }
      )
    )
  }

List Markup

  <div class="droparea" ondragover={cancel} ondragenter={cancel} ondrop={handleDragComplete}>
    <div key={item.id} class="row" for:each={items} for:item="item" for:index="index" draggable="true"
      ondragstart={handleDragStart} data-id={item.id}>
      {item.value}
    </div>
  </div>

Demo.

Related Topic