ReactでHTMLドラッグアンドドロップAPIを使用する方法
公開: 2022-03-10ドラッグアンドドロップAPIは、HTMLの最も優れた機能の1つです。 これは、Webブラウザにドラッグアンドドロップ機能を実装するのに役立ちます。
現在のコンテキストでは、ブラウザの外部からファイルをドラッグします。 ファイルをドロップすると、それらをリストに入れて名前を表示します。 ファイルが手元にあれば、クラウドサーバーにアップロードするなど、ファイルに対して他の操作を実行できます。
このチュートリアルでは、Reactアプリケーションでドラッグアンドドロップのアクションを実装する方法に焦点を当てます。 必要なのがプレーンなJavaScript
実装である場合は、最初に「Vanilla JavaScriptを使用してドラッグアンドドロップファイルアップローダーを作成する方法」を読みたいと思うでしょう。これは、JosephZimmermanが少し前に書いた優れたチュートリアルです。
dragenter
、 dragleave
、 dragover
、およびdrop
イベント
8つの異なるドラッグアンドドロップイベントがあります。 それぞれがドラッグアンドドロップ操作の異なる段階で起動します。 このチュートリアルでは、アイテムがドロップゾーンにドロップされたときに発生する4つ( dragenter
、 dragleave
、 dragover
、 drop
)に焦点を当てます。
- ドラッグされたアイテムが有効なドロップターゲットに入ると、
dragenter
イベントが発生します。 - ドラッグされたアイテムが有効なドロップターゲットを離れると、
dragleave
イベントが発生します。 - ドラッグ
dragover
イベントは、ドラッグされたアイテムが有効なドロップターゲット上にドラッグされているときに発生します。 (数百ミリ秒ごとに起動します。) -
drop
イベントは、アイテムが有効なドロップターゲットにドロップしたとき、つまりドラッグしてリリースしたときに発生します。
ondragover
およびondrop
イベントハンドラー属性を定義することにより、任意のHTML要素を有効なドロップターゲットに変えることができます。
MDN Webドキュメントから、8つのイベントに関するすべてを学ぶことができます。
Reactのドラッグアンドドロップイベント
開始するには、次のURLからチュートリアルリポジトリのクローンを作成します。
https://github.com/chidimo/react-dnd.git
01-start
ブランチをチェックしてください。 yarn
も取り付けられていることを確認してください。 あなたはyarnpkg.comからそれを得ることができます。
ただし、必要に応じて、新しいReactプロジェクトを作成し、 App.jsのコンテンツを次のコードに置き換えます。
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;
また、 App.cssのコンテンツを以下のCSSスタイルに置き換えます。
.App { margin: 2rem; text-align: center; } h1 { color: #07F; } .drag-drop-zone { padding: 2rem; text-align: center; background: #07F; border-radius: 0.5rem; box-shadow: 5px 5px 10px #C0C0C0; } .drag-drop-zone p { color: #FFF; } .drag-drop-zone.inside-drag-area { opacity: 0.7; } .dropped-files li { color: #07F; padding: 3px; text-align: left; font-weight: bold; }
リポジトリのクローンを作成した場合は、次のコマンドを(順番に)発行してアプリを起動します。
yarn # install dependencies yarn start # start the app
次のステップは、ドラッグアンドドロップコンポーネントを作成することです。 src/
フォルダー内にDragAndDrop.jsファイルを作成します。 ファイル内に次の関数を入力します。
import React from 'react'; const DragAndDrop = props => { const handleDragEnter = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = e => { e.preventDefault(); e.stopPropagation(); }; return ( <div className={'drag-drop-zone'} onDrop={e => handleDrop(e)} onDragOver={e => handleDragOver(e)} onDragEnter={e => handleDragEnter(e)} onDragLeave={e => handleDragLeave(e)} > <p>Drag files here to upload</p> </div> ); }; export default DragAndDrop;
return div
で、フォーカスHTML
イベントハンドラー属性を定義しました。 純粋なHTML
との唯一の違いは、キャメルケースであることがわかります。
onDragOver
およびonDrop
イベントハンドラー属性を定義したため、 div
は有効なドロップターゲットになりました。
これらのイベントを処理する関数も定義しました。 これらの各ハンドラー関数は、引数としてイベントオブジェクトを受け取ります。
イベントハンドラーごとに、 preventDefault()
を呼び出して、ブラウザーがデフォルトの動作を実行しないようにします。 デフォルトのブラウザの動作は、ドロップされたファイルを開くことです。 また、 stopPropagation()
を呼び出して、イベントが子要素から親要素に伝播されないようにします。
DragAndDrop
コンポーネントをApp
コンポーネントにインポートし、見出しの下にレンダリングします。
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>
次に、ブラウザでコンポーネントを表示すると、次の画像のようなものが表示されます。
リポジトリをフォローしている場合、対応するブランチは02-start-dragndrop
useReducer
フックを使用した状態の管理
次のステップは、各イベントハンドラーのロジックを作成することです。 その前に、ドロップされたファイルを追跡する方法を検討する必要があります。 ここから、状態管理について考え始めます。
ドラッグアンドドロップ操作中は、次の状態を追跡します。
-
dropDepth
これは整数になります。 これを使用して、ドロップゾーンの深さを追跡します。 後で、これをイラストで説明します。 (私のためにこれに光を当ててくれたEgor Egorovの功績です! ) -
inDropZone
これはブール値になります。 これを使用して、ドロップゾーン内にいるかどうかを追跡します。 -
FileList
これがリストになります。 ドロップゾーンにドロップされたファイルを追跡するために使用します。
状態を処理するために、ReactはuseState
フックとuseReducer
フックを提供します。 状態が前の状態に依存する状況を処理することを考慮して、 useReducer
フックを選択します。
useReducer
フックは、タイプ(state, action) => newState
のレデューサーを受け入れ、 dispatch
メソッドとペアになっている現在の状態を返します。
useReducer
の詳細については、Reactのドキュメントをご覧ください。
App
コンポーネント内( return
ステートメントの前)に、次のコードを追加します。
... const reducer = (state, action) => { switch (action.type) { case 'SET_DROP_DEPTH': return { ...state, dropDepth: action.dropDepth } case 'SET_IN_DROP_ZONE': return { ...state, inDropZone: action.inDropZone }; case 'ADD_FILE_TO_LIST': return { ...state, fileList: state.fileList.concat(action.files) }; default: return state; } }; const [data, dispatch] = React.useReducer( reducer, { dropDepth: 0, inDropZone: false, fileList: [] } ) ...
useReducer
フックは、レデューサーと初期状態の2つの引数を受け入れます。 現在の状態と、状態を更新するためのdispatch
関数を返します。 状態は、 type
とオプションのペイロードを含むアクションをディスパッチすることによって更新されます。 コンポーネントの状態に対して行われる更新は、アクションタイプの結果としてcaseステートメントから返される内容に依存します。 (ここで、初期状態はobject
であることに注意してください。)
状態変数ごとに、対応するcaseステートメントを定義して更新しました。 更新は、 useReducer
によって返されるdispatch
関数を呼び出すことによって実行されます。
次に、 data
を渡し、 App.jsファイルにあるDragAndDrop
コンポーネントにprops
としてdispatch
します。
<DragAndDrop data={data} dispatch={dispatch} />
DragAndDrop
コンポーネントの上部で、 props
から両方の値にアクセスできます。
const { data, dispatch } = props;
リポジトリをフォローしている場合、対応するブランチは03-define-reducers
です。
イベントハンドラーのロジックを完成させましょう。 省略記号は2つの線を表すことに注意してください。
e.preventDefault() e.stopPropagation() const handleDragEnter = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth + 1 }); }; const handleDragLeave = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth - 1 }); if (data.dropDepth > 0) return dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }) };
次の図では、ドロップゾーンAとBがネストされています。Aが対象のゾーンです。 これは、ドラッグアンドドロップイベントをリッスンする場所です。
ドロップゾーンにドラッグすると、境界に到達するたびに、 ondragenter
イベントが発生します。 これは、境界A-in
とB-in
発生します。 ゾーンに入っているので、 dropDepth
をインクリメントします。
同様に、ドロップゾーンからドラッグすると、境界に到達するたびに、 ondragleave
イベントが発生します。 これは、境界A-out
とB-out
で発生します。 ゾーンを離れるので、 dropDepth
の値をデクリメントします。 境界B-out
でinDropZone
をfalse
に設定していないことに注意してください。 そのため、dropDepthをチェックし、 0
より大きい関数dropDepth
から戻るためのこの行があります。
if (data.dropDepth > 0) return
これは、 ondragleave
イベントが発生した場合でも、ゾーンA内にいるためですA-out
をヒットした後でのみ、 dropDepth
が0
になり、 inDropZone
をfalse
に設定します。 この時点で、すべてのドロップゾーンを残しました。
const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };
このイベントが発生するたびに、 inDropZone
をtrue
に設定します。 これは、私たちがドロップゾーン内にいることを示しています。 また、 dataTransfer
オブジェクトのdropEffect
をcopy
に設定します。 Macでは、これは、ドロップゾーンでアイテムをドラッグするときに緑色のプラス記号を表示する効果があります。
const handleDrop = e => { ... let files = [...e.dataTransfer.files]; if (files && files.length > 0) { const existingFiles = data.fileList.map(f => f.name) files = files.filter(f => !existingFiles.includes(f.name)) dispatch({ type: 'ADD_FILE_TO_LIST', files }); e.dataTransfer.clearData(); dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 }); dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }); } };
ドロップされたファイルには、 e.dataTransfer.files
を使用してアクセスできます。 値は配列のようなオブジェクトであるため、配列拡散構文を使用してJavaScript
配列に変換します。
ここで、ファイルの配列に追加する前に、少なくとも1つのファイルがあるかどうかを確認する必要があります。 また、 fileList
にすでに存在するファイルを含めないようにします。 dataTransfer
オブジェクトは、次のドラッグアンドドロップ操作の準備としてクリアされます。 また、 dropDepth
とinDropZone
の値をリセットします。
DragAndDrop
コンポーネントのdiv
のclassName
を更新します。 これにより、 data.inDropZone
の値に応じて、 div
のclassName
が条件付きで変更されます。
<div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'} ... > <p>Drag files here to upload</p> </div>
data.fileList
を介してマッピングすることにより、 App.js内のファイルのリストをレンダリングします。
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop data={data} dispatch={dispatch} /> <ol className="dropped-files"> {data.fileList.map(f => { return ( <li key={f.name}>{f.name}</li> ) })} </ol> </div>
次に、いくつかのファイルをドロップゾーンにドラッグアンドドロップしてみます。 ドロップゾーンに入ると、 inside-drag-area
クラスがアクティブになっているため、背景の不透明度が低くなっていることがわかります。
ドロップゾーン内でファイルを解放すると、ドロップゾーンの下にファイル名が表示されます。
このチュートリアルの完全版は、 04-finish-handlers
ブランチにあります。
結論
HTML
ドラッグアンドドロップAPIを使用してReactでファイルのアップロードを処理する方法を見てきました。 また、 useReducer
フックを使用して状態を管理する方法も学びました。 ファイルhandleDrop
関数を拡張できます。 たとえば、必要に応じて、ファイルサイズを制限するための別のチェックを追加できます。 これは、既存のファイルのチェックの前または後に発生する可能性があります。 ドラッグアンドドロップ機能に影響を与えることなく、ドロップゾーンをクリック可能にすることもできます。
資力
- 「フックAPIリファレンス:
useReducer
」、React Docs - 「HTMLドラッグアンドドロップAPI」、MDNWebドキュメント
- 「DOMを使用したWebおよびXML開発の例」、MDN Web docs
- 「VanillaJavaScriptを使用してドラッグアンドドロップファイルアップローダーを作成する方法」、Smashing Magazine、Joseph Zimmerman
- 「Reactでの単純なドラッグアンドドロップファイルアップロード」、Egor Egorov、Medium