react-beautiful-dndで多階層構造にしたい

# はじめに
アプリ開発でドラッグアンドドロップ（DnD）を実装したかったのでreact-beautiful-dndを使用しました．多階層構造にしたかったため作り変えてみたので紹介します．

# やりたいこと
・列の要素数上限を4つにする．
・DnDによって4つを超えてしまったときは，移動先のはみ出した要素を移動元の配列に追加したい．

今回使用したライブラリは横の移動か縦の移動のみ対応していたため，移動先のはみ出た要素を入れ替えるようにした．

# 結論
実装したコードと完成図を紹介します．

![intro-dnd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2665507/00d70455-71b6-4eec-7fa1-951f06824f19.gif)

```App.js
import { useState } from "react";
import { Droppable, Draggable, DragDropContext } from "react-beautiful-dnd";
import "./App.css";

const getItems = (count) =>
  Array.from({ length: count }, (v, k) => k).map((k) => ({
    id: `item-${k}`,
    content: `item ${k}`,
  }));

/* 並び替え, 同ブロック */
const reorder = (list, startIndex, endIndex) => {
  const removed = list.splice(startIndex, 1); 
  list.splice(endIndex, 0, removed[0]); 
};

/* 並び替え, 異ブロック */
const move = (sourceList, destinationList, startIndex, endIndex) => {
  const removeDestination = destinationList.pop();
  const [removeSource] = sourceList.splice(startIndex, 1);
  destinationList.splice(endIndex, 0, removeSource);
  sourceList.unshift(removeDestination);
};

/* スタイル */
const grid = 8;

const getListStyle = (isDraggingOver) => ({
  background: isDraggingOver ? "lightblue" : "lightgrey",
  padding: grid,
  width: "348px",
  height: "53px",
  overflow: "hidden",
  display: "flex",
  flexWrap: "wrap",
});

const getItemStyle = (isDragging, draggableStyle) => ({
  userSelect: "none",
  padding: grid * 2,
  margin: `0 0 ${grid} 0`,
  width: "55px",
  background: isDragging ? "lightgreen" : "grey",
  ...draggableStyle,
});

function App() {
  const [items] = useState(getItems(12));

  /* 配列の要素数を最大4つにする*/
  const chunkSize = 4;
  const results = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    results.push(items.slice(i, i + chunkSize));
  }

  /* ドラッグアンドドロップしたとき*/
  const onDragEnd = (result) => {
    const { source, destination } = result;

    if (!destination) {
      return;
    }

    const start = parseInt(source.droppableId.charAt(source.droppableId.length - 1), 10);
    const end = parseInt(destination.droppableId.charAt(destination.droppableId.length - 1), 10);

    if (source.droppableId === destination.droppableId) {
      reorder(results[start], source.index, destination.index);
    } else {
      move(results[start], results[end], source.index, destination.index);
    }
  };

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      }}
    >
      <DragDropContext onDragEnd={onDragEnd}>
        <div>
          {results.map((result, index) => (
            <Droppable
              droppableId={`droppable-${index}`}
              direction="horizontal"
            >
              {(provided, snapshot) => (
                <div
                  {...provided.droppableProps}
                  ref={provided.innerRef}
                  style={getListStyle(snapshot.isDraggingOver)}
                >
                  {result.map((item, index) => (
                    <Draggable
                      key={item.id}
                      draggableId={item.id}
                      index={index}
                    >
                      {(provided, snapshot) => (
                        <div
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          style={getItemStyle(
                            snapshot.isDragging,
                            provided.draggableProps.style
                          )}
                        >
                          {item.content}
                        </div>
                      )}
                    </Draggable>
                  ))}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>
          ))}
        </div>
      </DragDropContext>
    </div>
  );
}

export default App;
```



# 解説
## 同じブロック内の移動
```App.js
const reorder = (list, startIndex, endIndex) => {
  const removed = list.splice(startIndex, 1);
  list.splice(endIndex, 0, removed[0]); 
};
```
reorderは同じブロック内の移動時に呼び出される．配列，移動元要素番号，移動先要素番号を引数に取る．
移動元要素を配列から削除して移動先の要素番号の場所に追加している．


## 異なるブロック間の移動
```App.js
const move = (sourceList, destinationList, startIndex, endIndex) => {
  const removeDestination = destinationList.pop();
  const [removeSource] = sourceList.splice(startIndex, 1);
  destinationList.splice(endIndex, 0, removeSource);
  sourceList.unshift(removeDestination);
};
```
moveは異なるブロック間の移動時に呼び出される．移動元配列，移動先配列，移動元要素番号，移動先要素番号を引数に取る．
1. まず，移動先の配列から最後の要素をポップする．
2. 次に移動元の要素を配列から削除する．
3. その後，移動先の配列に移動元要素を追加する．
4. 最後にポップした要素を移動元の配列の先頭に追加する．

これによってレイアウトが崩れなくなった．


## 横並びの要素数
```App.js
  const chunkSize = 4;
  const results = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    results.push(items.slice(i, i + chunkSize));
  }
```
今回は横に4つ並ぶように指定した．12つの要素数であるitemsを4つの要素数のresultsへ小分けにした．
TypeScriptの場合は，never型だ!って怒られるから型指定必要．


## JSX
```App.js
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      }}
    >
      <DragDropContext onDragEnd={onDragEnd}>
        <div>
          {results.map((result, index) => (
            <Droppable
              droppableId={`droppable-${index}`}
              direction="horizontal"
            >
              {(provided, snapshot) => (
                <div
                  {...provided.droppableProps}
                  ref={provided.innerRef}
                  style={getListStyle(snapshot.isDraggingOver)}
                >
                  {result.map((item, index) => (
                    <Draggable
                      key={item.id}
                      draggableId={item.id}
                      index={index}
                    >
                      {(provided, snapshot) => (
                        <div
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          style={getItemStyle(
                            snapshot.isDragging,
                            provided.draggableProps.style
                          )}
                        >
                          {item.content}
                        </div>
                      )}
                    </Draggable>
                  ))}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>
          ))}
        </div>
      </DragDropContext>
    </div>
  );

```

ライブラリが用意してくれているものが多いため，ほぼ呪文．resultsという元の配列からresultという4つの要素からなる配列にしていく．そのresultの要素をitemとして一つずつ出力している．

# 参考
https://www.youtube.com/watch?v=1amkEbOZM8M

https://github.com/eltociear/react-beautiful-dnd-ja