Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What are the problem?
@nakamo-03

【React/React Hook Form/react-beautiful-dnd】ReactでFormのパーツをドラッグ&ドロップさせる

はじめに

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で取得したデータなどを差し込んで使用したりイメージだといいかもしれません。

Form.tsx
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>
  );
}
Input.tsx
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 /> - ドラッグできる対象のもの
DragAndDrop.tsx
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だけ処理をするようにしています。

dragAndDropSetValue.tsx
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

Form.tsx
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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
5
Help us understand the problem. What are the problem?