【React】Trello風にドラッグ&ドロップを実装してみたい人へ

Reactがある程度使えるようになったので、チュートリアルとしてTrelloクローンを作ってみたいと思いました。

Shin

おお、タスク管理アプリのあれね

Todoアプリの拡張みたいなもので、すぐに作れるかと思ったのですがこれが何と難しいこと。なぜならドラッグ&ドロップをReactで実装しないといけないからです。

そんなお悩みを抱えている私とあなた、私が調べてきたので備忘録として残しておきます。

【React】Trello風にドラッグ&ドロップを実装してみた

今回用意するものがこちら👇

  • react-beautiful-dndライブラリ

ライブラリを使用します。私が1から自作する訳がありません。というか、できません。公式リファレンスを見たい方はこちらから。

何はともあれインポートしておきましょうか。

#yarn
yarn add react-beautiful-dnd

#npm
npm install react-beautiful-dnd --save

これで準備万端です。

①ドラッグアンドドロップする領域を指定する

ドラッグ&ドロップを実装したい領域をまずは指定してあげる必要があります。以下の図の「DragDropContext」領域内でしかドラッグ&ドロップすることができないので注意しましょう。

  • <DragDropContext /> ドラッグ&ドロップできる範囲のこと
  • <Droppable /> ドロップできる範囲のこと
  • <Draggable /> ドラッグできる範囲(要素)のこと

上の3つのコンポーネントを必ず用意する必要があります。まずは<DragDropContext />でドラッグ&ドロップできる範囲を指定してあげますね。

import React from "react";
import { DragDropContext } from "react-beautiful-dnd"; //import

function App() {
  return (
    <DragDropContext>  //ドラッグ&ドロップする範囲指定
      <div>item-0</div>
      <div>item-1</div>
      <div>item-2</div>
    </DragDropContext>
  );
}
export default App;

範囲指定をしました。範囲指定しただけなのでこれだけだとまだ使えません。この中にDroppableやDraggableを差し込んで使えるような形にしていきたと思います。

②DroppableとDraggableを挿入してみよう

ドラッグ&ドロップできる形にしていきます。

import React from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";

function App() {
  return (
    <DragDropContext>
      <Droppable> //ドロップできる範囲を指定
        {() => (
          <div className="droppableArea">
            <Draggable> //ドラッグしたい要素を指定
              {() => <div>item-0</div>}
            </Draggable>
            <Draggable> //ドラッグしたい要素を指定
              {() => <div>item-1</div>}
            </Draggable>
            <Draggable> //ドラッグしたい要素を指定
              {() => <div>item-2</div>}
            </Draggable>
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
export default App;

7行目の<Droppable>でドロップする範囲を決めます。その中身は関数で記述する必要があるので注意しましょう。関数を書かないとTypeError:children is not a funtionというエラーが吐かれてしまいます。

<Droppable>の中身にさらに<Draggable>を複数挿入します。こちらも中身は関数で記述する必要があるので注意しましょう。これでドラッグできる要素を決定しました。

コンソールに出てくるエラーを修正しよう

Shin

あれ、コンソールで何かエラー吐かれました!!

Invariant failed: A Droppable requires a droppableId propということはID指定を忘れいていたということですね。エラーで指定されたところだけ修正してみました👇

return (
    <DragDropContext>
      <Droppable droppableId="droppable">
        {() => (
          <div className="droppableArea">
            <Draggable draggableId={"item-0"} index={0}> //idとindexを指定
              {() => <div>item-0</div>}
            </Draggable>
            <Draggable draggableId={"item-1"} index={1}> //idとindexを指定
              {() => <div>item-1</div>}
            </Draggable>
            <Draggable draggableId={"item-2"} index={2}> //idとindexを指定
              {() => <div>item-2</div>}
            </Draggable>
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );

draggableIdindexを指定しました。これを指定しないとどの要素を現在ドラッグしているのか識別できないというわけですね。ちなみに今は直書きで番号を振っていますが最終的にはmap関数を使って簡単にIDを割り振るのでしばしお待ちください。

Shin

で、でもまだエラーが出てくる・・・

ここは言葉で説明するよりコードをみた方が早いです👇

return (
    <DragDropContext>
      <Droppable droppableId="droppable">
        {(provided) => (
          <div className="droppableArea" ref={provided.innerRef}> //refプロパティにprovided.innerRefを付与する
            <Draggable draggableId={"item-0"} index={0}>
              {(provided) => <div ref={provided.innerRef}>item-0</div>} //refプロパティにprovided.innerRefを付与する
            </Draggable>
            <Draggable draggableId={"item-1"} index={1}>
              {(provided) => <div ref={provided.innerRef}>item-1</div>} //refプロパティにprovided.innerRefを付与する
            </Draggable>
            <Draggable draggableId={"item-2"} index={2}>
              {(provided) => <div ref={provided.innerRef}>item-2</div>} //refプロパティにprovided.innerRefを付与する
            </Draggable>
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );

このinnerRefを付与することによって、ドラッグしている要素「以外」のドラッグされていない要素の動きを制御することができます(詳細はドキュメントに書いてあります)。

ちなみにここで呼び出しているprovidedは引数として受け取っているので、名前は何でもOKです。とにかくinnerRefを付与してあげる必要があります。

続いて出てくるエラーがこちら👇

Invariant failed: Draggable[id: item-0]: Unable to find drag handle

drag handle(ドラッグ操作)が見つかりませんとのことなので、<Draggable>で必要なdraghandlePropsなるものを設定する必要があります。

<Draggable draggableId={"item-0"} index={0}>
  {(provided) => (
    <div ref={provided.innerRef} {...provided.dragHandleProps}> //dragHandlePropsを追加
       item-0
    </div>
  )}
</Draggable>

👆一部だけコードを切り取っています。他のItemにも同じようにdragHandlePropsを付与してあげてください。

 

この状態で要素をマウスでホバーしてみてください。マウスカーソルの形が掴む形に変わったと思います。

Shin

おお!でも掴んでも要素移動できないぞ??

あと1つだけDraggableにプロパティを付与してください。

<Draggable draggableId={"item-0"} index={0}>
  {(provided) => (
    <div ref={provided.innerRef} 
     {...provided.dragHandleProps}>
         {...provided.draggableProps} //draggablePropsを追加
       item-0
    </div>
  )}
</Draggable>

これで要素移動ができるようになったと思います。

後はコンソールにワーニングが出るのでその対処をしておきます。+現時点での全体のコードを載せておきます。

import React from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";

function App() {
  return (
    <DragDropContext>
      <Droppable droppableId="droppable">
        {(provided) => (
          <div className="droppableArea" ref={provided.innerRef}>
            <Draggable draggableId={"item-0"} index={0}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  item-0
                </div>
              )}
            </Draggable>
            <Draggable draggableId={"item-1"} index={1}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  item-1
                </div>
              )}
            </Draggable>
            <Draggable draggableId={"item-2"} index={2}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  item-2
                </div>
              )}
            </Draggable>
            {provided.placeholder} //ここにplaceholoderを追加
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
export default App;

placeholderというものをDroppableの内部に付与して置いてください。これはドラッグ中に必要に応じてDroppable内のスペースを可変できるようにするためのものです。これは本ライブラリを使う上でのお作法だと思った方が良いでしょう。

③順番入れ替え操作を実装

あとはドラッグ&ドロップが終了したらその順番を入れ替えるだけです!

ドロップが終了したら順番を入れ替える必要があるので<DragDropContext>onDrapEndプロパティを付与してあげます。そしてその中に順番を入れ替える関数を記述してみましょう。

...省略

function App() {
  //ドラッグ&ドロップが終了したら呼ばれる関数
  const onDragEnd = (result) => {
    console.log(result); //中身の確認
  };
  return (
    <DragDropContext onDragEnd={onDragEnd}> //ここにonDragEndを追加
      <Droppable droppableId="droppable">
        {(provided) => (...以下省略

onDragEndプロパティはドラッグ&ドロップが終了したら呼ばれるものになります。onDragEnd関数を渡して、引数にresultを付与してコンソールで中身を確認してみました👇

ドラッグ開始情報(source)とドラッグ終了情報(destination)が入っているのが分かります。これでドラッグ開始と終了の2つの要素のインデックスが分かり、それを交換することができますね。

Shin

あと一息!

最後のコードになります👇

import React from "react";
import { useState } from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";

function App() {
  const [items] = useState(["item-0", "item-1", "item-2"]); //itemsの配列をuseStateで管理
  //ドラッグ&ドロップが終了したら呼ばれる関数
  const onDragEnd = (result) => {
    console.log(result); //中身を確認
    const remove = items.splice(result.source.index, 1); //ドラッグ開始要素を1つ削除
    items.splice(result.destination.index, 0, remove[0]); //ドラッグ終了の要素インデックスに削除した要素(remove[0])を挿入
  };
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="droppable">
        {(provided) => (
          <div className="droppableArea" ref={provided.innerRef}>
            <Draggable draggableId={"item-0"} index={0}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  {items[0]}  //配列でテキストを取得
                </div>
              )}
            </Draggable>
            <Draggable draggableId={"item-1"} index={1}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  {items[1]}  //配列でテキストを取得
                </div>
              )}
            </Draggable>
            <Draggable draggableId={"item-2"} index={2}>
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  {...provided.dragHandleProps}
                  {...provided.draggableProps}
                >
                  {items[2]}  //配列でテキストを取得
                </div>
              )}
            </Draggable>
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
export default App;

splice()メソッドを使って要素の削除と追加を行っています。詳しくはこちらの記事をご覧ください。

これでドラッグ&ドロップの実装が全て終了しました!!!!👏👏

Shin

ライブラリ使ったのに難しかったよ・・・

おまけ:map関数とスタイルを当てた最終形態

これだと見栄えとコーディングが煩雑で、要素数を増やしたり減らしたりしにくいので最終形態のコードを載せておきます👇

import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import React, { useState } from "react";
const getItems = (count) =>
  Array.from({ length: count }, (v, k) => k).map((k) => ({
    id: `item-${k}`,
    content: `item ${k}`,
  }));

const reorder = (list, startIndex, endIndex) => {
  //itemsの中身を1つずつ取り出して新しい配列にしている。
  console.log(startIndex); //0
  console.log(endIndex); //1
  console.log(list);
  const [removed] = list.splice(startIndex, 1);
  console.log(removed);
  list.splice(endIndex, 0, removed);
};

const grid = 8;

const getItemStyle = (isDragging, draggableStyle) => ({
  userSelect: "none",
  padding: grid * 2,
  margin: `0 0 ${grid}px 0`,
  background: isDragging ? "lightgreen" : "grey",
  ...draggableStyle,
});
const getListStyle = (isDraggingOver) => ({
  background: isDraggingOver ? "lightblue" : "lightgrey",
  padding: grid,
  width: 250,
});
function App() {
  const [items] = useState(getItems(10));
  //ドラッグが終わったらの処理
  const onDragEnd = (result) => {
    //リストの外にドロップされたら
    if (!result.destination) {
      return;
    }
    //順番を並び替える
    reorder(
      items,
      result.source.index, //ドラッグ開始配列インデックス
      result.destination.index //ドラッグ目的先終了インデックス
    );
    //並べたitemsをセットする。
  };
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="droppable">
        {(provided, snapshot) => (
          <div
            {...provided.droppableProps}
            ref={provided.innerRef}
            style={getListStyle(snapshot.isDraggingOver)}
          >
            {items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided, snapshot) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    style={getItemStyle(
                      snapshot.isDragging,
                      provided.draggableProps.style
                    )}
                  >
                    {item.content}
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
export default App;

動かない人はGitにも載せておくのでクローンして試してみてください。👆の最終形態はサンプルをそのまま参考にしました。

参考にしたサイトたち

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です