🌏
[Next.js/React]dnd-kitのsortableの階層構造のコンポーネントでの簡単な実装
dnd-kitが便利だったのでまとめました。
次のサンプルように、階層ごとにSortableContextで並び替えられるようにしたいとします。
dnd-kitの公式ドキュメントによると、DndContextはnestできるとのこと。
DndContext | @dnd-kit – Documentation https://docs.dndkit.com/api-documentation/context-provider
以下のような実装が可能です(上記サイト引用)
import React from 'react';
import {DndContext} from '@dnd-kit/core';
function App() {
return (
<DndContext>
{/* Components that use `useDraggable`, `useDroppable` */}
<DndContext>
{/* ... */}
<DndContext>
{/* ... */}
</DndContext>
</DndContext>
</DndContext>
);
}
以下のコードのように再帰構造で作ります。
"use client";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { DndContext } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import { FC } from "react";
import { useState } from "react";
type Props = {
name: string;
children: Props[];
}
const DndTest = () => {
const [items, setItems] = useState<Props>({
name: 'Root', children: [{
name: 'A', children: [
{name: '1', children: []},
{name: '2', children: []}
]
},{
name: 'B', children: [
{name: '1', children: []},
{name: '2', children: [
{name: 'a', children: []},
{name: 'b', children: []}
]}
]
},{
name: 'C', children: [
{name: '1', children: []},
{name: '2', children: [
{name: 'a', children: []},
{name: 'b', children: []},
{name: 'c', children: []}
]},
{name: '3', children: []}
]
}]
});
const SortableItem = ({id, item}: {id: string; item: Props;}) => {
const {
setNodeRef, attributes, listeners, transform, transition, isDragging,
} = useSortable({
id,
animateLayoutChanges: () => false
});
return (
<div
ref={setNodeRef}
{...attributes}
{...listeners}
style={{
transform: CSS.Translate.toString(transform), // Translateに変更
transition,
}}
>
<div className="my-2 px-5 border border-gray-300 rounded-md bg-white">
<p>{item.name}</p>
{buildView(item, id)}
</div>
</div>
);
};
const buildView = (_item: Props, id: string) => {
const handleDragEnd = (event: any) => {
const { active, over } = event;
if (!over) return;
if (active.id !== over.id) {
const oldIndex = Number(active.id.toString().split('.').slice(-1))
const newIndex = Number(over.id.toString().split('.').slice(-1))
const newArray = [..._item.children]
newArray.splice(newIndex, 0, newArray.splice(oldIndex, 1)[0])
_item.children = newArray
setItems({...items})
}
}
return (
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={_item.children.map((item, index) => `${id}.${index}`)}>
{_item.children.map((item, index) => (
<SortableItem key={index} id={`${id}.${index}`} item={item}/>
))}
</SortableContext>
</DndContext>
)
}
return (
<div className="flex justify-center m-3">
<div className="w-72 m-2 px-5 border border-gray-300 rounded-md bg-cyan-500">
{buildView(items, 'id')}
</div>
</div>
);
}
export default DndTest;
このとき、次の点に注意
- onDragEndで順番を入れ替えた時、domを再描画する必要があるが、useStateは最上位のコンポーネントでないと使えないので、最上位のstateを更新する必要がある。
- SortableContextのitemsプロパティと、useSortableのidは完全一致させる。
実装面
- buildViewを再帰的に呼んで階層構造にしてます。
-
CSS.Transform.toString(transform)
からCSS.Translate.toString(transform)
に変更。これをしないと高さが違う要素を並び替えるときにゆがんだりする。
参照:https://github.com/clauderic/dnd-kit/issues/117 -
animateLayoutChanges: () => false
を追加
参照:https://stackoverflow.com/questions/76746110/dnd-kit-sortable-animation-is-triggering-twice-on-dragend - 再帰構造でidが一意なものとなるようにid.0.1.1のようにindexが連なるようにしています。
Discussion