'How to Drag & Drop Multiple Elements In React?

This is my first question on StackOverflow. I want to build a little game with React, where users can drag and drop tetrominos onto a grid and also reposition or rotating them to their liking. The tetrominos are represented by a matrix and then every block gets rendered in a li element.

Example for the z-tetromino: [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]

Unfortunately I cannot post images yet, that would make things easier.

The grid is too respresented by a matrix.

Now what I want to do is basically drag and drop these block matrices onto the grid, so that the values in the grid change accordingly (0 → 1 etc.).

The problem is, I have no clue how to drag multiple li elements at once with the standard HTML5 DnD API or with React DnD. When the user clicks on one li element of a certain tetromino, the whole piece should move. Maybe I could solve this using jQuery UI, but since in React no direct DOM manipulation is allowed, I'm left wondering how to do it.

I tried to drag one block onto the grid which worked semi optimally, because one block took the place of an entire row of grid blocks, even with display: inline-block set in CSS.

Here is some simple code from the first experiment.

onDragStart = e => {
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text', e.target.id);
    // e.dataTransfer.setDragImage(e.target.parentNode, 20, 20);
  };

  handleDrop = e => {
    const pieceOrder = e.dataTransfer.getData('text');
    // console.log(document.getElementById(pieceOrder));
    // e.target.appendChild(document.getElementById(pieceOrder));
    // console.log(pieceOrder);
    e.target.replaceWith(document.getElementById(pieceOrder));
    e.target.remove();
  };

renderEmptyBoardCell(i) {
    return (
      <li key={i} className="emptyBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
    );
  }

  renderTemplateBoardCell(i) {
    return (
      <li key={i} className="templateBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
    );
  }

  renderEmptyCell(i) {
    return (
      <li key={i} className="emptyCell"></li>
    );
  }

  renderFilledCell(piece_config, i) {
    return (
      <li key={i} id={i} className={`filledCell ${piece_config}`} draggable onDragStart={this.onDragStart}></li>
    );
  }

So the question is, would that be theoretically possible with React DnD or any other library? If yes, what would be the approximate solution to DnD multiple elements at once.

Thanks for your time!



Solution 1:[1]

You can only drag one item at a time using react-dnd. Either use a different library, or somehow group together the different pieces into one item first, and then drag and drop that one item.

Solution 2:[2]

In case anyone is looking for a solution in 2020. Here is my current solution with react-dnd and react hooks. You can try the live demo here.

Here is another simpler example, you can check out the codesandbox here.

Solution 3:[3]

I know its a bit late but have you looked into: panResponder. I am looking into multiple d'n'd elements and panResponder is the most likely fit

Solution 4:[4]

A nice choice for your need could be react-beautiful-dnd.

Solution 5:[5]

Try this out It will surely work in your case!

react-beautiful-dnd multi drag pattern

https://github.com/atlassian/react-beautiful-dnd/tree/master/stories/src/multi-drag

demo: https://react-beautiful-dnd.netlify.app/?path=/story/multi-drag--pattern

import React, { Component } from 'react';
import styled from '@emotion/styled';
import { DragDropContext } from 'react-beautiful-dnd';
import initial from './data';
import Column from './column';

import type { Result as ReorderResult } from './utils';
import { mutliDragAwareReorder, multiSelectTo as multiSelect } from './utils';

import type { DragStart, DropResult, DraggableLocation } from 'react-beautiful-dnd';
import type { Task, Id } from '../types';
import type { Entities } from './types';

const Container = styled.div`
  display: flex;
  user-select: none;
  justify-content: center;
`;

type State = {
  entities: Entities,
  selectedTaskIds: Id[],
  columnFlag: false,

  // sad times
  draggingTaskId: Id,
};

const getTasks = (entities: Entities, columnId: Id): Task[] =>
  entities.columns[columnId].taskIds.map(
    (taskId: Id): Task => entities.tasks[taskId],
  );

export default class TaskApp extends Component<any, State> {
  state: State = {
    entities: initial,
    selectedTaskIds: [],
    draggingTaskId: '',
  };

  componentDidMount() {
    window.addEventListener('click', this.onWindowClick);
    window.addEventListener('keydown', this.onWindowKeyDown);
    window.addEventListener('touchend', this.onWindowTouchEnd);
  }

  componentWillUnmount() {
    window.removeEventListener('click', this.onWindowClick);
    window.removeEventListener('keydown', this.onWindowKeyDown);
    window.removeEventListener('touchend', this.onWindowTouchEnd);
  }

  onDragStart = (start: DragStart) => {
    const id: string = start.draggableId;
    const selected: Id = this.state.selectedTaskIds.find(
      (taskId: Id): boolean => taskId === id,
    );

    // if dragging an item that is not selected - unselect all items
    if (!selected) {
      this.unselectAll();
    }
    this.setState({
      draggingTaskId: start.draggableId,
    });
  };

  onDragEnd = (result: DropResult) => {
    const destination = result.destination;
    const source = result.source;
    const draggableId = result.draggableId;  
    const combine  = result.combine;

    console.log('combine',combine);
    console.log('destination',destination);
    console.log('source',source);
    console.log('draggableId',draggableId);
    
    // nothing to do
    if (!destination || result.reason === 'CANCEL') {
      this.setState({
        draggingTaskId: '',
      });
      return;
    }

    const processed: ReorderResult = mutliDragAwareReorder({
      entities: this.state.entities,
      selectedTaskIds: this.state.selectedTaskIds,
      source,
      destination,
    });

    this.setState({
      ...processed,
      draggingTaskId: null,
    });
  };

  onWindowKeyDown = (event: KeyboardEvent) => {
    if (event.defaultPrevented) {
      return;
    }

    if (event.key === 'Escape') {
      this.unselectAll();
    }
  };

  onWindowClick = (event: KeyboardEvent) => {
    if (event.defaultPrevented) {
      return;
    }
    this.unselectAll();
  };

  onWindowTouchEnd = (event: TouchEvent) => {
    if (event.defaultPrevented) {
      return;
    }
    this.unselectAll();
  };

  toggleSelection = (taskId: Id) => {
    const selectedTaskIds: Id[] = this.state.selectedTaskIds;
    const wasSelected: boolean = selectedTaskIds.includes(taskId);
    
    console.log('hwwo',this.state.entities.columns);
    console.log('hwwo',this.state.entities.columns.done.taskIds);

    
    // if there is change in entities - update the state
        

    const newTaskIds: Id[] = (() => {
      // Task was not previously selected
      // now will be the only selected item
      if (!wasSelected) {
        return [taskId];
      }

      // Task was part of a selected group
      // will now become the only selected item
      if (selectedTaskIds.length > 1) {
        return [taskId];
      }

      // task was previously selected but not in a group
      // we will now clear the selection
      return [];
    })();

    this.setState({
      selectedTaskIds: newTaskIds,
    });
  };

  toggleSelectionInGroup = (taskId: Id) => {
    const selectedTaskIds: Id[] = this.state.selectedTaskIds;
    const index: number = selectedTaskIds.indexOf(taskId);

    // if not selected - add it to the selected items
    if (index === -1) {
      this.setState({
        selectedTaskIds: [...selectedTaskIds, taskId],
      });
      return;
    }

    // it was previously selected and now needs to be removed from the group
    const shallow: Id[] = [...selectedTaskIds];
    shallow.splice(index, 1);
    this.setState({
      selectedTaskIds: shallow,
    });
  };

  // This behaviour matches the MacOSX finder selection
  multiSelectTo = (newTaskId: Id) => {
    const updated: string[] | null | undefined = multiSelect(
      this.state.entities,
      this.state.selectedTaskIds,
      newTaskId,
    );

    if (updated == null) {
      return;
    }

    this.setState({
      selectedTaskIds: updated,
    });
  };

  unselect = () => {
    this.unselectAll();
  };

  unselectAll = () => {
    this.setState({
      selectedTaskIds: [],
    });
  };

  render() {
    const entities = this.state.entities;
    const selected = this.state.selectedTaskIds;

    console.log('entities', entities);
    console.log('selected', selected);
    
    return (
      <DragDropContext
        onDragStart={this.onDragStart}
        onDragEnd={this.onDragEnd}
      >
        <Container>
          {entities.columnOrder.map((columnId: Id) => (
            <Column
              column={entities.columns[columnId]}
              tasks={getTasks(entities, columnId)}
              selectedTaskIds={selected}
              key={columnId}
              draggingTaskId={this.state.draggingTaskId}
              toggleSelection={this.toggleSelection}
              toggleSelectionInGroup={this.toggleSelectionInGroup}
              multiSelectTo={this.multiSelectTo}
              entities={entities}
            />
          ))}
        </Container>
      </DragDropContext>
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dan Cron
Solution 2 Lala La
Solution 3 arnaud goudsmit
Solution 4 Grégory C
Solution 5