Next.jsでTrello風タスク管理アプリを作成する日記③

# はじめに
**タイトルが変わりました**
TODOアプリを作る中で空想が膨らみタスク管理アプリのTrelloみたいなものを作る事にしました。

https://www.atlassian.com/ja/software/trello/enterprise

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/a188274d-fc36-e08b-2881-0c7f8ef3898b.png)

# 前回までのあらすじ

**[Next.jsでTODOアプリを作成する日記①](https://qiita.com/naitoyuma/items/dd60203cdacc026afdf5)**
- [x] Tailwindを導入できた
- [x] ComponentにPropsを渡せた
- [x] Componentの中でPropsを使用できた
- [x] Componentをループを使って表示できた
- [x] React Iconsを導入できた

**[Next.jsでTODOアプリを作成する日記②](https://qiita.com/naitoyuma/items/409f2e14624d8983d0cd)**
- [x] useStateを使って状態管理ができた
- [x] setStateを使って状態の更新ができた
- [x] 状態に対するイミュータブル(不変)な操作の必要性が分かった
- [x] 子側で親のイベントをトリガーできた  


前回までの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/e4ceab38-368b-4986-8780-9469e4ef9cc1.png)

## 状態に応じてフィルターしたTodoリストを並べる
状態に応じてTodoリストにフィルターをかけ各タスクリストを状態に対応して横並びにしました。

![2023-11-21_11h10_12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/01540744-7f08-f060-c888-60bd74fde527.gif)

上記の修正についてポイントを絞って記載します。

状態を配列で定義しこれをループさせて各状態に応じたカラムを作成します
```tsx
// Status型に含まれませんが"All"という全ての状態のTodoを表示するカラムも作る事にします
const [statuses, setStatuses] = useState([
    "All",
    "Incomplete",
    "Progress",
    "Done",
]);
// useStateを使わないとDOM描画時にstatuses.lengthが取得できなかったです。
// (原因わかったら追記します)
```

各カラムとTodoItemを描画する部分
```tsx
return (
    <>
        {/* colsはstatusesに設定した状態の数に合わせる */}
        <div className={`grid grid-cols-${statuses.length}`}>
            {statuses.map((status, i) => {
                // filterを使用してTodoListの状態に応じてフィルタリングする
                const filteredTodoList = todoItemList.filter(
                    // statusが"All"の場合はフィルタリングしない
                    (item) => status === "All" || item.status === status
                );
            
                return (
                    <div key={i} className="mx-2 px-4 py-2 rounded-lg bg-gray-200">
                        {/* statusに対応したタグを設置 */}
                        <span
                            className="inline-flex items-center py-1.5 px-3 mb-1 rounded-full text-xs font-medium bg-gray-500 text-white">
                            {status}
                        </span>
                        {/* filterしたTodoListをmapで回してTodoを描画*/}
                        {filteredTodoList.map((todo, j) => (
                            <div key={j}>
                                <TodoItem {...todo} />
                            </div>
                        ))}
                        {/* statusがAllの時だけtTodoFormを設置 */}
                        {status === "All" && <TodoForm addTodoOnclick={addTodoOnClick} />}
                    </div>
                );
            })}
            </div>
		</>
```
TodoItemで各状態に応じたIcon描画も追加しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/6d36eba5-6efa-3980-09c9-54aaefc923db.png)

今更ですがtsxの関数内ではDOMをそのまま変数に定義できる事に驚きました。
```tsx:components/todoItem.tsx
// 状態に応じたIconのElementを定義しています。
switch (props.todo.status) {
    case "Done":
        ...
        statusValues.bgColor = "bg-emerald-500";
        // ここでElementをそのまま変数に入れてる
        statusValues.iconDom = (
            <FaCheckCircle className="w-6 h-6 text-white fill-current" />
        );
        break;
    case "Progress":
        ...
        statusValues.bgColor = "bg-blue-600";
        // ここでElementをそのまま変数に入れてる
        statusValues.iconDom = (
            <TbProgress className="w-6 h-6 text-white fill-current" />
        );
        break;
    case "Incomplete":
        ...
        statusValues.bgColor = "bg-gray-600";
        // ここでElementをそのまま変数に入れてる
        statusValues.iconDom = (
            <RiZzzFill className="w-6 h-6 text-white fill-current" />
        );
        break;
}
```
コンポーネントの中でこんな感じで色に関するstyleとセットでiconを描画しています
```tsx
<div className={`w-12 ${statusValues.bgColor}`}>
    {statusValues.iconDom}
</div>
```

## ドラッグ&ドロップで並べ替える
ドラック＆ドロップでカラム内のTodoコンポーネントの並べ替えを作成したいと思います。
下記サンプルではカラム内だけでなくカラムを横断した並び替えを実装していますが、ここではまずはカラム内の並び替えのみを実装します。

https://react-beautiful-dnd.netlify.app/?path=/story/board--simple

下記サイトでライブラリを比較を参考にドラック中の描画がいい感じだったdnd kitというライブラリを使用したいと思います。

> [Reactのドラッグ&ドロップソートライブラリを比較](https://tech-blog.talentio.co.jp/entry/2023/03/28/143944) 

公式サイトにサンプルコードもありましたので参考にしながらアプリに仕込みました。

https://dndkit.com/

わーい(^^)/
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/311f10e7-8ef4-430e-7b5c-634e88c6e30b.gif)


上記ではアプリとして不足部分がありますが、ここまでの手順をdnd kitの仕組みと合わせポイントを絞ってまとめます。

**DnD導入**
```
npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
```

**Dnd kitを仕込む**
DnD kitを組み込んだコード部分のみ記載します。
```tsx:todoListForm.tsx
return (
    <>
        <div className={`grid grid-cols-${statuses.length}`}>
            {statuses.map((status, i) => {
                const filteredTodoList = todoItemList.filter(
                    (item) => status === "All" || item.status === status
                );

                return (
                    <div key={i} className="mx-2 px-4 py-2 rounded-lg bg-gray-200">
                        <span className="inline-flex items-center py-1.5 px-3 mb-1 rounded-full text-xs font-medium bg-gray-500 text-white">
                            {status}
                        </span>

                        {status !== "All" ? (
                            <>
                                // ここから
                                <DndContext
                                    sensors={sensors}
                                    collisionDetection={closestCenter}
                                    onDragEnd={handleDragEnd}>
                                    {filteredTodoList.map((todo, j) => (
                                        <SortableContext
                                            key={todo.id}
                                            items={todoItemList}
                                            strategy={verticalListSortingStrategy}>
                                            <TodoItem todo={todo} isSortable />
                                        </SortableContext>
                                    ))}
                                </DndContext>
                                // ここまで
                            </>
                        ) : (
                            filteredTodoList.map((todo, j) => (
                                <TodoItem key={todo.id} todo={todo} />
                            ))
                        )}
                        {status === "All" && <TodoForm addTodoOnclick={addTodoOnClick} />}
                    </div>
                );
            })}
        </div>
    </>
);
```

以下のサイトではカラム間のドラック＆ドロップが解説付きで実装されているため、DnD kitを知りたい方をすぐにこれらの記事を開いて下さい。

https://zenn.dev/kodaishoituki/articles/0e1c6109ae838e

https://zenn.dev/aldagram_tech/articles/c2cf248bd016fc


### senser
- ドラッグの開始、移動、終了をどのような操作で実行するか設定する
　ポインター操作とキーボード操作を設定しています。
- KeyboardSensorにcoordinateGetter:sortableKeyboardCoordinatesを設定する事で矢印キー押下時に隣のアイテムと入れ替えられるみたいです

```tsx
const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
        coordinateGetter: sortableKeyboardCoordinates,
    })
);
```


collisionDetection設定：[衝突検出のアルゴリズム](https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithms)
もっとも近い要素を取得するclosestCenterを設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/45c3d534-6177-4c5e-1578-7d83c5cbc8a1.png)

### Handler
onDragEndにドロップしたの際の処理を設定します。

>[DndContext](https://docs.dndkit.com/api-documentation/context-provider)

ドラック＆ドロップによる入れ替わりに必要な処理は自作する必要があります。
ドラックさせるデータをどんな構造で保持しているかは開発者によって異なるため、入れ替わりの処理もそれらに対応させなければいけないのだと思います。
```ts:データ構造
// 現状のデータ構造はシンプルな配列です
data:Todo[] = [
{
    id: 1,
    title: "タイトル",
    content: "TODO内容はここに記載します。",
    status: "Done",
},
{
    id: 2,
    title: "タイトル2",
    content: "TODO内容の二番目",
    status: "Progress",
},
{
    id: 3,
    title: "タイトル3",
    content: "TODO内容の3番目",
    status: "Incomplete",
},
]
```

ドラック＆ドロップの動きをデータに紐づけるため
1. SortableContextのitems propsにバインドしたいデータ配列を設定
2. keyに配列内の各データに一意の値を設定
```tsx
<SortableContext
    /* keyの値を設定するためTodoにidを持たせました
       idは後に記述するHandelrで使用します */
    key={todo.id}
    // itemsは入れ替わりが発生するデータ配列を設定します
    items={todoItemList}
    strategy={rectSortingStrategy}>
    ...
</SortableContext>
```
またTodoItem側でも渡されたidをuseSortableに設定しています。
keyとしてもidを渡しているのですがkeyとuseSortableのidは一致させておく必要がありました。
その辺の処理内容をきちんと調べてないです。
```tsx
<div
    // ↓この辺はよく分からない
    ref={setNodeRef}
    {...attributes}
    {...listeners}
    // ソートが可能か否かisSortableというpropsに渡して対応したstyleを設定してます。
    style={props.isSortable ? style : { cursor: "default" }}>
    ...
</div>
```
上記の設定でドラック操作対象のTodoのidやドロップした場所にあるTodoのidを取得する事ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/1539f5c9-7f8c-869e-cb4b-93f637abf0a6.png)

上記のidをHandler内で利用し入れ替わりを実装します。
```tsx
const handleDragEnd = (event: DragEndEvent) => {
     
    const { active, over } = event;
   
    if (!over) {
        return;
    }

    // 別のTodo上にドロップした場合(over.idとactive.idが異なる)
    if (active.id !== over.id) {
        const oldIndex = todoItemList.findIndex((v) => v.id === active.id);
        const newIndex = todoItemList.findIndex((v) => v.id === over.id);
        // arrayMoveを使用して配列内の順番を入れ替える
        setTodoList(arrayMove(todoItemList, oldIndex, newIndex));
    }
};

```

HandlerはDndContextに以下のようにpropsで渡します。
現在はonDragEnd propsとして設定しドロップしたタイミングで発火させます
```tsx
<DndContext
    sensors={sensors}
    collisionDetection={closestCenter}
    onDragEnd={handleDragEnd}>
    ...
</DndContext>
```
ちょっと見にくいですがConsoleにactive.idとover.idを出力するとこのようになります。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601137/680b083b-7456-0bd6-f438-727fd8b1df59.gif)

データが入れ替わった後、その情報が即座にDOMに反映されるのはuseStateを使用してデータの状態を管理し、ドロップ後のhandlerでsetStateを使用しているためです。


またアニメーションやスタイルはDnd kitが用意したstyleをTodoItem側に設定しています。
```tsx
// ドラック＆ドロップに伴うスタイルを設定
const style = {
    transform: CSS.Transform.toString(transform),
    transition,
};

// 先ほども記載したこの部分
<div
    ref={setNodeRef}
    {...attributes}
    {...listeners}
    // ソートが可能か否かisSortableというpropsに渡して対応したstyleを設定してます。
    style={props.isSortable ? style : { cursor: "default" }}>
```

まだカラム間のドラック＆ドロップを実装していませんが今回はここで一旦区切ります。

# まとめ
私にとってはライブラリが充実している事より、ライブラリの記事が充実している方が大切だと感じました。
みなさん分かりやすい記事をありがとう…

- [x] filterをかけて条件に一致したデータでComponentを表示できた
- [x] Dnd kitを導入できた
- [x] Dnd kitのDndContextとソート機能に関するSortableContextでカラム内ソートを実装できた

④へ続く
[Next.jsでTrello風タスク管理アプリを作成する日記④](https://qiita.com/naitoyuma/items/8261d3da9b9b9c4312f2)