🤛
React Hook Form(RHF)の配列操作をドラッグアンドドロップ(DnD)にしたい
はじめに
React Hook Form(RHF)とdnd kitを用いる。
DnDについて確認したいというのが主なので、RHFについてはFormのSubmitなどはしていない。その点ご了承ください。そもそもForm書いてないですし。
RHFという略称は伝わるのだろうか、といつも疑問だ。
package.jsonのバージョン情報は以下。
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"react-hook-form": "^7.54.2",
実装の前に
実は私のやろうとしていること(書いている時点ではやったこと)は既に記事になっていた。
重複となるが、許してほしい。実装
Form.tsx
"use client";
import {
useForm,
useFieldArray,
useWatch,
FormProvider,
useFormContext,
UseFieldArrayRemove,
UseFieldArrayInsert,
FieldValues,
} from "react-hook-form";
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import {
FaGripVertical,
FaMinus,
FaPlus,
} from "react-icons/fa6";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
const FormChild = ({
item,
index,
remove,
insert,
}: {
item: Record<"id", string>;
index: number;
remove: UseFieldArrayRemove;
insert: UseFieldArrayInsert<FieldValues, "test">;
}) => {
const { register } = useFormContext();
const {
isDragging,
// 並び替えのつまみ部分に設定するプロパティ
setActivatorNodeRef,
attributes,
listeners,
// DOM全体に対して設定するプロパティ
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}
className="grid w-1/2 grid-cols-1"
>
<div
className="flex items-center justify-between gap-4"
key={item.id}
>
<div
ref={setActivatorNodeRef}
style={{
cursor: isDragging ? "grabbing" : "grab",
}}
{...attributes}
{...listeners}
>
<FaGripVertical />
</div>
<input
{...register(`test.${index}.value`)}
className="w-1/2 rounded-md p-2 text-black"
/>
<button
type="button"
onClick={() => remove(index)}
className="rounded-full border bg-red-500 p-2 hover:opacity-75"
>
<FaMinus />
</button>
</div>
<button
type="button"
onClick={() => insert(index + 1, { value: "" })}
className="flex flex-row items-center opacity-50 hover:bg-gray-500 hover:opacity-100"
>
<div className="rounded-full bg-blue-400">
<FaPlus />
</div>
<div className="w-full border-b-2 border-blue-400" />
</button>
</div>
);
};
const DraggableForm = () => {
const methods = useForm();
const { control } = methods;
const { fields, append, remove, move, insert } =
useFieldArray({
control,
name: "test",
});
const watchValues = useWatch({ control: control });
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over === null) return;
if (active.id !== over.id) {
const oldIndex = fields.findIndex(
(item) => item.id === active.id
);
const newIndex = fields.findIndex(
(item) => item.id === over.id
);
move(oldIndex, newIndex);
}
};
return (
<div className="p-4">
<div className="grid grid-cols-1 gap-4">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={fields}
strategy={verticalListSortingStrategy}
>
<FormProvider {...methods}>
{fields.map((field, index) => (
<FormChild
key={field.id}
item={field}
index={index}
remove={remove}
insert={insert}
/>
))}
</FormProvider>
</SortableContext>
</DndContext>
{JSON.stringify(watchValues)}
</div>
<button
type="button"
onClick={() => append({ value: "" })}
className="rounded-md border p-2 hover:bg-gray-500"
>
追加ボタン
</button>
</div>
);
};
export default DraggableForm;
終わりに
これは面白い! すべてをDnDにしたくなってきた。
ネストした配列に対してどのような挙動をとるかは未検証。
できれば、テーブルカラムもDnDにしたい。
参考文献
Discussion