しばしばドラッグ&ドロップで要素を移動した際に値も受け渡したい場合があります。しかなしながら素の JavaScript のドラッグ&ドロップのイベントでは要素そのものか文字列ぐらいしか渡せません。このため次の様にドラッグイベント発生時にどこかに値を保持し、ドロップ時にそれを参照する、という方法が欲しくなります。
HTMLElement: drop イベント – Web API | MDN中のデモを引用
<html lang="ja"> <head> <meta charset="utf-8"> <title></title> <style> body { /* 例でユーザーがテキストを選択するのを防ぐ */ user-select: none; } #draggable { text-align: center; background: white; } .dropzone { width: 200px; height: 20px; background: blueviolet; margin: 10px; padding: 10px; } </style> </head> <body> <div class="dropzone"> <div id="draggable" draggable="true"> この div はドラッグ可 </div> </div> <div class="dropzone" id="droptarget"></div> <script> let dragged = null; const source = document.getElementById("draggable"); source.addEventListener("dragstart", event => { // ドラッグ中の要素の参照を保存 dragged = event.target; }); const target = document.getElementById("droptarget"); target.addEventListener("dragover", event => { // ドロップできるように既定の動作を停止 event.preventDefault(); }); target.addEventListener("drop", event => { // 既定の動作(一部の要素でリンクとして開く)を行わないようにする。 event.preventDefault(); // ドラッグした要素を選択されたドロップターゲットに移動する if (event.target.className == "dropzone") { dragged.parentNode.removeChild(dragged); event.target.appendChild(dragged); } }); </script> </body> </html>
上記 MDN の例ではドラッグイベント発生時に dragged に要素の参照を保存し、ドロップイベント発生時にそれを呼び出して DOM を変更することによって要素の移動を表現しています。これを React でも同様に行います。これの方法は状態管理ライブラリを使うことで実装できます。ここでは jotai を使います(window 以下にプロパティを生やす方法でもできますが、グローバルオブジェクトの汚染はなるべく避けるべきなので、ここでは紹介しません)。
jotai はRact 用の状態管理ライブラリです。状態管理ライブラリを使うことによって props のバケツリレーする必要がなくなります。一方で React が動いている間、値がグローバルな領域に残り続けるため乱雑に使うとプログラムに混乱をもたらすという欠点もあります。
pmndrs/jotai: 👻 Primitive and flexible state management for React
【React】手軽なグローバル状態管理ライブラリ jotai の紹介 – 株式会社シーポイントラボ | 浜松のシステム・RTK-GNSS開発
この jotai を用いてドラッグ開始時にデータを状態に保存し、ドロップ時にデータを状態から取り出す、という方針でコードを作ります。
実際に作ったデモとコードが次です。
クリックでソースコードを展開
import { DragEvent, DragEventHandler, useCallback } from 'react'; import { atom, useAtom } from 'jotai'; // 最後にドラッグしたデータを保持する atom const lastDraggedItemAtom = atom<unknown>(null); /** ドラッグ&ドロップのイベントハンドラーを返すフック */ export const useDragAndDrop = <T = unknown>(): { makeHandleDragStart: ( passItem: T, callback?: (passItem: T, event: DragEvent<HTMLElement>) => void ) => DragEventHandler<HTMLElement>; makeHandleDrop: (callback?: (passItem: T, event: DragEvent<HTMLElement>) => void) => DragEventHandler<HTMLElement>; } => { // ドラッグ時に渡されるアイテムを保持する atom のセッター、ゲッター。 useState 的に使えます。 const [lastDraggedItem, setLastDraggedItem] = useAtom(lastDraggedItemAtom); /** ドラッグ開始イベントハンドラー関数を返す関数 */ const makeHandleDragStart = useCallback( (passItem: T, callback?: (passItem: T, event: DragEvent<HTMLElement>) => void): DragEventHandler<HTMLElement> => (event) => { // ハンドラー生成時に渡された値をセットする setLastDraggedItem(passItem); // 通常の onDragStart と似た形のコールバックを呼び出し側で定義でき、それを使う様にしておく callback && callback(passItem, event); }, [setLastDraggedItem] ); /** ドロップイベントハンドラー関数を返す関数 */ const makeHandleDrop = useCallback( (callback?: (passItem: T, event: DragEvent<HTMLElement>) => void): DragEventHandler<HTMLElement> => (event) => { // 通常の onDrop と似た形のコールバックを呼び出し側で定義でき、それを使う様にしておく // このコールバックに最後にドラッグされた時にセットされた値を渡すことで任意のデータ受け渡しを実現する callback && callback(lastDraggedItem as T, event); }, [lastDraggedItem] ); return { makeHandleDragStart, makeHandleDrop }; };
import React from "react"; import "./styles.css"; import { useDragAndDrop } from "./useDragAndDrop"; type Company = { name: string; }; /** ドラッグ可能要素。props で渡された会社名をドロップ先に渡せるようなドラッグイベントを起こす */ const DraggableItem: React.FC<{ name: string }> = ({ name }) => { const { makeHandleDragStart } = useDragAndDrop<Company>(); return ( <div className="drag-item" draggable onDragStart={makeHandleDragStart({ name })} > ドラッグできます。{JSON.stringify({ name })} </div> ); }; /** ドロップ可能要素。ドラッグ時にセットされた会社名を読み取って表示する */ const DropArea: React.FC = () => { const [passedItem, setPassedItem] = React.useState<Company | null>(null); const { makeHandleDrop } = useDragAndDrop<Company>(); const handleDrop = makeHandleDrop((item) => { setPassedItem(item); }); return ( <div className="drop-area" onDrop={handleDrop} onDragOver={(e) => e.preventDefault()} > ドロップできます。{JSON.stringify(passedItem)} </div> ); }; export default function App() { return ( <div className="App"> <h1>atomを使ったドラッグ&ドロップ例</h1> <DraggableItem name="シーポイント" /> <DraggableItem name="ラボ" /> <DropArea /> <DropArea /> </div> ); }
こんな感じでドラッグ&ドロップ時に自由にデータを受け渡しできます。これができると web アプリケーション的な複雑な機能も作りやすくなります。