2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Next.js]はじめてのdnd-kit その2(単一コンテナ並べ替え)

Last updated at Posted at 2024-09-07

はじめに

Next.jsを触ってみたいと、学習用に簡単なアプリを作っており、その中で新たに試したことを記事にしています。
同じようにこれからNext.js始めてみようという方の参考になれば嬉しいです。

やりたいこと

アイテムをドラッグアンドドロップで並び替えを行いたい。
今回は、単一コンテナ内でのアイテムの並び替えとなります。

前回dnd-kitの基本編を記事にしました。
続きとして「dnd-kit/sortable」を使っていきます。

「はじめてのdnd-kit」シリーズ全4回の2回目です。

前回分はこちら

続きはこちら

環境

下記のDocker開発環境にて行います。

基本コンポーネント

イメージ

DndContext

・全体のドラッグアンドドロップコンテキストを提供する最上位コンポーネント
・ドラッグアンドドロップのロジック全体を管理する親コンポーネントで、この中でイベントリスナーや設定を行い、すべてのドラッグ操作を処理
・主にonDragEndやonDragStartなど、ドラッグイベントをハンドリングするためのプロパティを渡す

SortableContext

・並べ替えが可能なアイテムのリストを囲むコンテキストコンポーネント
・ドラッグ可能なアイテムのコンテナとして機能し、並べ替え可能なグループを定義
・このコンテキスト内に並べ替えるアイテムを配置し、itemsというプロパティを渡し、その中のアイテムが並べ替え可能であることを示す

useSortable

・個々のアイテムをドラッグアンドドロップ可能にするためのフック
・useDraggableとuseDroppableがまとまったイメージ
・ドラッグ操作中の状態も管理

コンポーネントファイル作成

こちらのドキュメントに沿って単一コンテナの並べ替えを作成します。

インストール

利用するにはdnd-kit/sortableをインストールします。

npm install @dnd-kit/sortable

Sortable.tsx

まずはuseSortableを使ってドラッグアンドドロップするアイテム用のコンポーネントを作成します。

・最低限idはuseSortableの引数として渡す必要があります。
transition:useSortableの引数として設定することで、durationの秒数や、遷移タイミング関数を設定できます。
使い所が難しいですが下記のような設定して変わった動きをさせることも出来ます。

  } = useSortable({
    transition: {
      duration: 500,
      easing: 'cubic-bezier(0.25, 1, 1, -0.68)',
    },
    id: id
  });

・isDragging:ドラッグ中にTrueとなりstyle内に設定すれば、ドラッグ中だけ色を変えたり出来ます。
attributes,listeners,setNodeRef,transformはuseDraggableと似ているので割愛)

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { UniqueIdentifier } from '@dnd-kit/core';

type SortableProps = {
  children: React.ReactNode;
  id: UniqueIdentifier;
};

export function Sortable({children, id}: SortableProps) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({
    id: id
  });
  
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    backgroundColor: isDragging ? 'green' : undefined,
  };
  
  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {children}
    </div>
  );
}

SortableItem.tsx

ドラッグアンドドロップするアイテム自体のコンポーネントを作成。
サンプルなので、idを表示するだけのカードにします。

import { UniqueIdentifier } from "@dnd-kit/core"

export function SortableItem ({itemId}: {itemId:UniqueIdentifier}) {
  return (
    <div className='w-full h-24 flex justify-center items-center border-2 border-dashed border-slate-300/50'>
      {`id:${itemId}`}
    </div>
  )
}

全体:サンプルの単一コンテナ

先に全体がこちらになります。

image.png

page.tsx
"use client";

import { useState } from 'react';
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { Sortable } from '@/util/dndkit/Sortable';
import { SortableItem } from '@/components/cards/SortableItem';
import { DndContext, DragEndEvent } from '@dnd-kit/core';

export default function Page() {
  const [items, setItems] = useState(
    {
      id: 'list-sample',
      title: 'List Sample',
      cards: [
        {id: 'card-1', title: 'Card 1'},
        {id: 'card-2', title: 'Card 2'},
        {id: 'card-3', title: 'Card 3'},
      ],
    },
  );

  function handleDragEnd(event: DragEndEvent) {
    const {active, over} = event;
    if (!over)return;
    if (active.id === over.id) return;
    const oldSortable = active.data.current?.sortable;
    const newSortable = over.data.current?.sortable;
    if (!oldSortable || !newSortable) return;
    
    setItems({
        ...items,
        cards: arrayMove(items.cards, oldSortable.index, newSortable.index),
      });
  }
  
  return (
    <div className='flex justify-center items-center w-screen h-screen'>
      <DndContext
        onDragEnd={handleDragEnd}
        id={items.id}
      >
        <SortableContext items={items.cards} key={items.id} id={items.id}>
          <div className='flex flex-col gap-4 border w-44 p-4'>
            {items.cards.map((card) => (
              <Sortable key={card.id} id={card.id}>
                <SortableItem itemId={card.id}/>
              </Sortable>
            ))}
          </div>
        </SortableContext>
      </DndContext>
    </div>
  );

}

サンプルデータ

今回はシンプルなサンプルデータを使います。

  const [items, setItems] = useState(
    {
      id: 'list-sample',
      title: 'List Sample',
      cards: [
        {id: 'card-1', title: 'Card 1'},
        {id: 'card-2', title: 'Card 2'},
        {id: 'card-3', title: 'Card 3'},
      ],
    },
  );

コンポーネントの組み合わせ

DndContext
一番外側に配置します。
後ほど記載しますが、ドロップ完了後の動作をonDragEndで指定します。
SortableContext
その次にSortableContextを配置します。
ここでitemsに並べ替えをするアイテムグループを渡して並べ替えアイテムを定義できます。
div(column)
リストを縦に並べたかったので、アイテムを並べる枠を作成。
Sortable,SortableItem
cards配列からmap関数で取り出して作成したSortable.tsxとSortableItem.tsxを使ってカードを配置。
反復して配置するため、keyとidにcard.idを指定。
表示用にSortableItemにもitemIdとしてcard.idを指定。
  return (
    <div className='flex justify-center items-center w-screen h-screen'>
      <DndContext
        onDragEnd={handleDragEnd}
        id={items.id}
      >
        <SortableContext items={items.cards}>
          <div className='flex flex-col gap-4 border w-44 p-4'>
            {items.cards.map((card) => (
              <Sortable key={card.id} id={card.id}>
                <SortableItem itemId={card.id}/>
              </Sortable>
            ))}
          </div>
        </SortableContext>
      </DndContext>
    </div>
  );

下記Issueに書かれているとおりServer側とClient側でaria-describedbyが相違してしまい
エラー「Warning: Prop aria-describedby did not match.
Server: "DndDescribedBy-1" Client: "DndDescribedBy-0"」が発生し、idを設定することで解消できました。

handleDragEnd(function)について

ドロップ完了時の動作を設定します。

イベントデータを取得

DragEndEventからactiveとoverを取得し、下記の時は即返却します。
・overがない:キャンセルされた
・active.idとover.idが同じ:同じ場所にドロップされた

  function handleDragEnd(event: DragEndEvent) {
    const {active, over} = event;
    if (!over)return;
    if (active.id === over.id) return;

並べ替え前後のデータを取得

active,overはそれぞれ下記のようなデータになっています。

active
{
    "id": "card-1",
    "data": {
        "current": {
            "sortable": {
                "containerId": "Sortable-0",
                "index": 0,
                "items": [
                    "card-1",
                    "card-2",
                    "card-3"
                ]
            }
        }
    },
    "rect": {
        "current": {
            "initial": null,
            "translated": null
        }
    }
}
over
{
    "id": "card-2",
    "rect": {
        "width": 142,
        "height": 112,
        "top": 342.5,
        "bottom": 454.5,
        "right": 600,
        "left": 458
    },
    "data": {
        "current": {
            "sortable": {
                "containerId": "Sortable-0",
                "index": 1,
                "items": [
                    "card-1",
                    "card-2",
                    "card-3"
                ]
            }
        }
    },
    "disabled": false
}

上記から、data.current.sortableを取得します。

    const oldSortable = active.data.current?.sortable;
    const newSortable = over.data.current?.sortable;
    if (!oldSortable || !newSortable) return;

新しいリストとしてitemsを更新

上記で取得したSotableからIndexを取得してarrayMoveで入れ替えた配列を取得
arrayMoveは、配列・移動元のインデックス・移動先のインデックスの3つを渡して、入れ替えた配列を返してくれます。

スプレッド構文でitemsを展開し、入れ替えた配列でcardsを上書きし、seItemsでセットします。

    setItems({
        ...items,
        cards: arrayMove(items.cards, oldSortable.index, newSortable.index),
      });

arrayMoveについて

中身はArray.spliceを使って、削除と追加をやっているものでした。

/**
 * Move an array item to a different position. Returns a new array with the item moved to the new position.
 */
export function arrayMove<T>(array: T[], from: number, to: number): T[] {
  const newArray = array.slice();
  newArray.splice(
    to < 0 ? newArray.length + to : to,
    0,
    newArray.splice(from, 1)[0]
  );

  return newArray;
}

テスト

無事入れ替えが出来ました。

入れ替え.gif

つづき

さいごに

・単一コンテナで順番を並べ替えるだけならシンプルですがこちらで十分そうです。
・次回はもう少し難しそうな複数コンテナの並び替えに挑戦したいと思います。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?