Создаём drag-and-drop элементы на React

Отредактировано

В этой статье рассмотрим создание drag-and-drop элементов на React с помощью библиотеки react-beautiful-dnd от Atlassian.

55К открытий57К показов

В этой статье рассмотрим создание drag-and-drop элементов на React с помощью библиотеки react-beautiful-dnd от Atlassian. Статья рассчитана на людей, знакомых с React.

Вы научитесь создавать drag-and-drop элементы на React и сможете создать игру, наподобие такой:

Простая игра с drag-and-drop на React

Основные концепции

DragDropContext: место (поле), где фактически осуществляется drag-and-drop. Этот компонент вызывает onDragEnd после того, как перетаскиваемый объект был отпущен. Также можно определить onDragStart и onDragUpdate для вызова после начала перетаскивания и по какому-нибудь событию во время перетаскивания соответственно.

Droppable: компонент, откуда и куда перетаскивается элемент. Этому компоненту нужны несколько свойств, которые будут описаны далее.

Draggable: элемент, который будет перемещаться. Как и droppable, ему нужны некоторые свойства, чтобы сделать компонент перемещаемым.

Создание игры

Определим начальные настройки, которые нужны для создания игры:

			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;
		

Теперь можно перейти к созданию первого компонента. Он будет содержать только один метод render. Собственно, вот он:

			class NumbersGame extends React.Component{
    
  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: это просто обёртка для DragDropContext.
VerticalColumn: это столбец, содержащий перетаскиваемые элементы.

			export default (props: IVerticalColumnProps) =>
		

Droppable

DroppableWrapper — это компонент, который реализует droppable-контейнер. У него есть необходимые свойства, которые определяются так:

			export default (props: any) =>
  
    {(provided: any) => (
      
              {props.children}
      
    )}
		

Здесь нужен droppableId, который должен быть уникальным в рамках DragDropContext. Он также ожидает функцию в качестве дочернего элемента и использует паттерн render props, который позволяет избежать внешнего вмешательства в DOM.

Первый аргумент этой функции — provided. У этого аргумента есть droppableProps и они важны для определения компонента как droppable. Здесь можно применить некоторые или все из возможных свойств с помощью spread-синтаксиса. Для более подробной информации можно посмотреть документацию.

Второе свойство — это innerRef, функция, которая передаёт необходимый DOM-элемент библиотеке.
Последнее свойство — placeholder. Этот элемент при необходимости используется для увеличения пространства в droppable-области во время перетаскивания. Placeholder нужно сделать дочерним элементом droppable-компонента. На этом этапе настройка компонента закончена.

Draggable

Теперь можно перейти к написанию DraggableListItems. Этот компонент создаёт NumberBox (перетаскиваемые объекты).

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

function toNumberBox(item: INumberItemProps, position: number) {
  return 
}
		

NumberBox определяет обёртку для draggable:

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

DraggableItemWrapper реализует draggable, потому что как и в случае droppable у него есть нужные свойства.

			export default (props: any) =>
  
    {(provided: any) => (
      
        {props.children}
      
    )}
		

В этом коде реализуется уже упомянутый выше паттерн render props. В данном случае у draggable есть два свойства: draggableId и index. DraggableId должен быть уникален в рамках DragDropContext. Он также принимает функцию как дочерний элемент. Первый аргумент функции — provided (как и в droppable). Второй аргумент — dragHandleProps. Это свойство определяет компонент как draggable.

Теперь вы можете использовать DraggableItemWrapper и забыть про низкоуровневые свойства.

onDragEnd

Вот что уже реализовано:

  • NumberGameContext: реализация DragDropContext.
  • DroppableWrapper: реализация droppable.
  • DraggableItemWrapper: реализация draggable.

А вот те компоненты, которые являются частью игры:

  • VerticalColumn: компонент, который инкапсулирует droppable-столбцы и draggable-элементы.
  • DraggableListItems: компонент, инкапсулирующий все NumberBox-элементы.
  • NumberBox: собственно, переносимый элемент.

Но пока onDragEnd остаётся пустым. Исправим это.
Первым делом пройдёмся по аргументам метода:

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

Аргумент содержит данные об элементе, который перетаскивают (из какого столбца и позиции) и о месте, куда перетаскивают элемент (в какой столбец и позицию), а также какое действие сейчас происходит: «drag» или «drop».

Первым делом сохраним destination, draggableId и source в переменные:

const { destination, source, draggableId } = result;

Теперь создадим новый список отсортированных элементов:

			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));
		

И обновим состояние:

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

И изюминка игры: как только пользователь перетаскивает элемент или же выигрывает, то воспроизводится соответствующая мелодия из папки assets:

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

Вот так выглядит весь метод:

			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);
}
		

Заключение

Тех компонентов, которые были описаны выше, хватит для создание группы элементов с возможностью их перетаскивания в рамках одного столбца. Если в игру добавить CSS и дополнительные звуки, то это сделает её более приятной.

Ресурсы

Следите за новыми постами
Следите за новыми постами по любимым темам
55К открытий57К показов