はじめに
React Hook Form で作成したフォーム内のinputをドラッグ&ドロップさせる機能を作ったのでアウトプット。
使用ライブラリ
- React 17.0.1
- React Hook Form ^7.0.0
- react-beautiful-dnd 13.1.1
いきなりどうでもいいことかもしれませんが
drag and drop を dadではなくdndで訳すのは andのAを発音しないパターンのやつだからぽいです。
ロックンロール(Rock and Roll)と同じ考え方?という解釈
参照記事: https://kenjimorita.jp/why-draganddrop-dnd/
ライブラリ選定について
ライブラリ選定において、軽く調べたものをzennにscrapにしてます。(ほんとに軽くしか調べていない)
https://zenn.dev/yuto_nakamoto/scraps/b9793f51c65bec
実装
formのコード
同じようなInputをdata分 mapを使って表示させます。
今回は仮データにしています。本来はapiで取得したデータなどを差し込んで使用したりイメージだといいかもしれません。
import React from "react";
import { useForm } from "react-hook-form";
import { Input } from "./Input"
interface UseFormInputs {
mailAddress: string
password: string
}
export default function Form() {
// 今回は仮データ、本来はapiで取得したデータなどを差し込んで使用したりする。
const formdata = [{id: 1, value:'test'},{id: 2, value:'test2'},{id: 3, value:'test3'}]
const [data, setData] = useState(formdata)
const methods = useForm<UseFormInputs>();
const { register, handleSubmit, reset } = methods
const onSubmit = (data: UseFormInputs) => {
console.log('test')
};
return (
<div>
<h1>リセット試す用のフォームです</h1>
<form onSubmit={handleSubmit(onSubmit)}>
{
data.map((item, index) => {
return (<Input data={item} index={index} methods={methods}/>)
})
}
<input type="submit" />
</form>
</div>
);
}
export const Input = (props) => {
return(
<input
type="text"
{...props.methods.register(`text[${props.index}]`)}
defaultValue={props.data.value}
/>
}
)
DragAndDrop実装
react-beautiful-dndを使って実装する
-
<DragDropContext />
- ドラッグ&ドロップを有効にするアプリケーションの部分をラップするドM -
<Droppable />
- ドロップできるエリア 中にDraggbleDomが必要 -
<Draggable />
- ドラッグできる対象のもの
import React from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { dragAndDropSetValue } from '../../../utils/dragAndDropSetValue'
export const DragAndDrop = (props) => {
const { getValues, setValue } = props.methods
// ドラッグ&ドロップ時の挙動
function handleOnDragEnd(result: any) {
const items = Array.from(props.data);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
props.setData(items);
dragAndDropSetValue(result.destination.index, result.source.index, getValues, setValue)
}
// ドラッグ&ドロップ対象にpropsが必要なのでcloneElementでなんとかしている。
const DragAndDropItem = (props) => {
const { children, ...newProps } = props;
const childrenWithProps = React.Children.map(children, (child: React.ReactElement) => React.cloneElement(child, { ...newProps }));
return (
<div key={props.index} ref={props.provided.innerRef} {...props.provided.draggableProps} {...props.provided.dragHandleProps}>
{childrenWithProps}
</div>
)
}
return (
props.data && props.data.length !== 0 ?
<div>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{props.data.map((item, index) => {
return (
<Draggable key={item.id} draggableId={`${item.id}`} index={index}>
{(provided) => (
<DragAndDropItem index={index} provided={provided} item={item} methods={props.methods}>
{props.children}
</DragAndDropItem>
)}
</Draggable>
)
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
:
null
)
}
DragAndDrop時にformの値を変更させる関数を用意する
こちらはタイトル通りDragAndDrop時にformの値を変更させる関数を用意しました。
すべてのformの値が変わると重い処理になってしまうので、DragAndDrop時に関連するformの値のみ変更するように条件分岐を追加しています。
例) 1-2-3 が 1-3-2 になったとき 1は値を変更する必要がないので2と3だけ処理をするようにしています。
import { UseFormSetValue } from "react-hook-form"
// destinationIndex: 移動先, sourceIndex: 移動元
export const dragAndDropSetValue = (destinationIndex: number, sourceIndex: number, getValues, setValue) => {
const formValue = getValues('text')
if (formValue === undefined) return
if (destinationIndex < sourceIndex) {
for (let i: number = destinationIndex; i <= sourceIndex; i++) {
const settingValue = i === destinationIndex ? formValue[sourceIndex] : formValue[i - 1]
const setValueTarget = `text[${i}]`
setValue(setValueTarget, settingValue)
}
} else if (sourceIndex < destinationIndex) {
for (let i = sourceIndex; i <= destinationIndex; i++) {
const settingValue = i === destinationIndex ? formValue[sourceIndex] : formValue[i + 1]
const setValueTarget = `text[${i}]`
setValue(setValueTarget, settingValue)
}
}
}
form部分にドラッグ&ドロップ用のコンポーネントを組み込む
ドラッグ&ドロップ用のコンポーネントを作成したので、それを組み込んでいきます。
もともとInputにpropsを渡していたのですが、こちらをなくしています。
ですがInputへは、DragAndDropコンポーネント側からpropsを付与しています。
渡し方については別の記事で書いていますのでそちらを参考にしてみてください。
https://qiita.com/nakamo-03/items/abf2634c2c3b068e0220
import React from "react";
import { useForm } from "react-hook-form";
interface UseFormInputs {
mailAddress: string
password: string
}
export default function Form() {
// 今回は仮データ、本来はapiで取得したデータなどを差し込んで使用したりする。
const formdata = [{id: 1, value:'test'},{id: 2, value:'test2'},{id: 3, value:'test3'}]
const [data, setData] = useState(formdata)
const { register, handleSubmit, reset } = useForm<UseFormInputs>();
const onSubmit = (data: UseFormInputs) => {
console.log('test')
};
return (
<div>
<h1>リセット試す用のフォームです</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<DragAndDrop
data={array}
setData={setData}
methods={methods}
>
<Input/>
</DragAndDrop>
<input type="submit" />
</form>
</div>
);
}
終わりに
ドラッグ&ドロップとformを組み合わせると、結構レンダリング回数が増えてしまうのでstateの管理や、formの値の変更は慎重に行う必要があることがわかりました。
あと、ちょっと雑な部分がある+実際に動く画面みた方がいい箇所もあったりするかもなので追記がんばります。
参考
React Hook Form ドキュメント
なぜDragAndDrop(ドラッグアンドドロップ)を「DnD」というの?なぜ「DaD」じゃないの
react-beautiful-dnd を使ってドラッグ&ドロップ機能を実装する
Github: react-beautiful-dnd