» » Creating drag-and-drop elements with React

Creating drag-and-drop elements with React

In this tutorial, we'll look at creating drag-and-drop elements in React using Atlassian library . The article is intended for people familiar with React.

You will learn how to create drag-and-drop elements in React and be able to create a game like this:

Creating drag-and-drop elements with React

Basic Concepts


DragDropContext: The place (field) where the drag-and-drop is actually performed. This component is called ondragEndafter the dragged object has been released. It can also be defined ondragStartto ondragUpdatebe called after the start of the drag and on some event during the drag, respectively.

Droppable : The component where the element is being dropped from and to. This component needs several properties, which will be described next.

Draggable : The element that will be dragged. Like droppable , it needs some properties to make the component moveable.



Creating a game


Let's define the initial settings that are needed to create the game:

const initialData = {
  column: {
    id: 'column-1',
      numberIds: ['four', 'one', 'five', 'three', 'two'],
    },
    numbers: {
      'five': { id: 'five', content: '5' },
      'four': { id: 'four', content: '4' },
      'one': { id: 'one', content: '1' },
      'three': { id: 'three', content: '3' },
      'two': { id: 'two', content: '2' },
    }
};

export default initialData;


Now we can move on to creating the first component. It will contain only one method render. Actually, here it is:

class NumbersGame extends React.Component<any, INumbersGameState>{

  public constructor(props: any) {
    super(props);
    this.onDragEnd = this.onDragEnd.bind(this);
    this.state = {...initialData};
  }

  public onDragEnd(result: any) {
    // Элемент отпущен!
  }

  public render() {
    const numbers = this.state.column.numberIds.map((numberId: string) => this.state.numbers[numberId]);

    return (
      <NumbersGameContext onDragEnd={this.onDragEnd}>
       <VerticalColumn column={this.state.column} items={numbers} />
      </NumbersGameContext>
    )
  }
}


NumbersGameContext: it's just a wrapper for DragDropContext.
VerticalColumn: This is the column containing the draggable items.

export default (props: IVerticalColumnProps) =>
  <DroppableWrapper droppableId={props.column.id} className="source">
    <DraggableListItems items={props.items} />
  </DroppableWrapper>


Droppable

DroppableWrapperis a component that implements a droppable container. It has the necessary properties, which are defined like this:

export default (props: any) =>
  <Droppable droppableId={props.droppableId}>
    {(provided: any) => (
      <div className={props.className}
            ref={provided.innerRef}
            {...provided.droppableProps}
            {...provided.droppablePlaceholder}>
              {props.children}
      </div>
    )}
  </Droppable>


It needs a droppableId, which must be unique within the DragDropContext. It also expects a function as a child element and uses the render props pattern to avoid external DOM tampering.

The first argument to this function is provided. This argument has droppablePropsand is important for defining the bean as a droppable . Here you can apply some or all of the possible properties using the spread syntax. For more details, see the documentation .

The second property is innerRef, a function that passes the required DOM element to the library.
The last property is placeholder. This element is optionally used to increase the space in the droppable area while dragging. Placeholdershould be made a child of the droppable component. At this stage, the configuration of the component is completed.

Draggable

Now we can move on to writing DraggableListItems. This component creates NumberBox(dragged objects).

export default (props: IDraggableListItems) =>
  <div> {props.items.map(toNumberBox)} </div>

function toNumberBox(item: INumberItemProps, position: number) {
  return <NumberBox key={item.id}
                    className="box"
                    itemPosition={position}
                    value={item.id}
                    content={item.content} />
}


NumberBoxdefines a wrapper for draggable :

export default (props: IDraggableItem) => {
  const className = `dnd-number size-${props.value}`;

  return (
    <DraggableItemWrapper draggableId={props.value}
                          index={props.itemPosition}
                          className={className}>
      <div>{props.content}</div>
    </DraggableItemWrapper>
  )
}


DraggableItemWrapperimplements draggable because, like droppable , it has the right properties.

export default (props: any) =>
  <Draggable draggableId={props.draggableId} index={props.index}>
    {(provided: any) => (
      <div className={props.className}
           ref={provided.innerRef}
           {...provided.draggableProps}
           {...provided.dragHandleProps}>
        {props.children}
      </div>
    )}
  </Draggable>


This code implements the render props pattern already mentioned above . In this case, the draggable has two properties: draggableIdand index. DraggableIdmust be unique within DragDropContext. It also accepts a function as a child element. The first argument to the function is provided(as in droppable ). The second argument is dragHandleProps. This property defines the component as draggable .

Now you can use DraggableItemWrapperand forget about low-level properties.

ondragEnd

Here is what has already been implemented:

- NumberGameContext: realization DragDropContext.
- DroppableWrapper: droppable implementation.
- DraggableItemWrapper: draggable implementation.

And here are the components that are part of the game:

- VerticalColumn: A component that encapsulates droppable columns and draggable elements.
- DraggableListItems: A component that encapsulates all NumberBox elements.
- NumberBox: actually, a portable element.

But for now it ondragEndremains empty. Let's fix this.
First of all, let's go through the arguments of the method:

result: {
  destination: {
    droppableId: "column-1"
    index: 2
  }
  draggableId: "four"
  reason: "DROP"
  source: {
    droppableId: "column-1"
    index: 0
  }
  type: "DEFAULT"
}


The argument contains data about the element being dragged (from which column and position) and the place where the element is being dragged (to which column and position), as well as whether the action is now “ drag ” or “ drop ”.

First of all, save destination, draggableIdand sourceinto variables:

const { destination, source, draggableId } = result;

Now let's create a new list of sorted elements:

const column = this.state.column;
const numberIds = Array.from(column.numberIds);
numberIds.splice(source.index, 1);
numberIds.splice(destination.index, 0, draggableId);
const numbers = numberIds.map((numberId: string) =>
parseInt(this.state.numbers[numberId].content, 10));


And update the state:

const newColumn = {
 ...column,
 numberIds
};
this.setState({
 ...this.state,
 column: newColumn
});


And the highlight of the game: as soon as the user drags an element or wins, the corresponding melody from the folder is played assets:

public playSound(numbers: number[]) {
 const sound = isSortedAsc(numbers) ? ClapsSound : MoveSound;
 new Audio(sound).play();
}


This is how the whole method looks like:

public onDragEnd(result: any) {
  const { destination, source, draggableId } = result;
  if (!destination) { return }

  const column = this.state.column;
  const numberIds = Array.from(column.numberIds);
  numberIds.splice(source.index, 1);
  numberIds.splice(destination.index, 0, draggableId);
  const numbers = numberIds.map((numberId: string) =>
    parseInt(this.state.numbers[numberId].content, 10));

  this.playSound(numbers);
  this.updateState(column, numberIds);
}


Conclusion


Those components that were described above are enough to create a group of elements with the possibility of dragging them within one column. If you add CSS and additional sounds to the game, it will make it more enjoyable.

Related Articles

Add Your Comment

reload, if the code cannot be seen

All comments will be moderated before being published.