ホームページ > 記事 > ウェブフロントエンド > React、Tailwind CSS、Dnd-kit を使用してアイテムを配置/並べ替えるためのドラッグ アンド ドロップを実装する
Trello や Asana などのアプリケーションが直感的なドラッグ アンド ドロップ インターフェイスをどのように管理しているか疑問に思ったことはありますか?ユーザーがアイテムを簡単に並べ替える必要があるアプリケーションがあると想像してください。スムーズなドラッグ アンド ドロップ機能がないと、この作業は退屈でイライラするものになる可能性があります。このブログ投稿では、React、Tailwind CSS、Dnd-kit を使用して動的なドラッグ アンド ドロップ機能を実装し、アイテムの配置と並べ替えのためのシームレスなユーザー エクスペリエンスを作成する方法を検討します。
現実の世界では、アプリケーションは多くの場合、優先順位、ステータス、またはその他の基準に基づいて項目を並べ替えることをユーザーに要求します。たとえば、ユーザーはブレインストーミング セッション中にアイデアをすばやく並べ替える必要がある場合があります。効率的なドラッグ アンド ドロップ機能がないと、このプロセスには項目の位置を手動で編集したり、非効率的な上へ移動/下へ移動ボタンを使用したりするなど、面倒な手順が必要になる可能性があります。私たちの目標は、このプロセスを簡素化し、ユーザーにとってより直感的で効率的なものにするソリューションを提供することです。
ユーザーがアイデアを整理できるブレインストーミング ツールの使用例を考えてみましょう。ユーザーには次の機能が必要です:
新しいアイデアをリストに追加します。
これらのアイデアをドラッグ アンド ドロップして希望の順序に並べ替え、優先順位を付けます。
異なるカテゴリ間でアイデアを移動します (新しいアイデアと古いアイデアなど)。
これを実現するには、プロジェクトのセットアップに Vite、スタイル設定に Tailwind CSS、ドラッグ アンド ドロップ機能に Dnd-kit を使用して React アプリケーションを構築します。この設定により、生産性とユーザー エクスペリエンスを向上させる使いやすいインターフェイスを作成できるようになります。
npm create vite@latest my-drag-drop-app --template反応
cd my-drag-drop-app
npm インストール
npm install tailwindcss dnd-kit 反応-ホット-トースト 反応-アイコン
npx tailwindcss init
module.exports = { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, ], plugins: [], }
@tailwind base; @tailwind components; @tailwind utilities;
App.jsx ファイルは、アプリケーション全体のレイアウトを設定し、グローバルな状態を管理する主要コンポーネントです。
概要
アプリケーション全体の状態を管理するメインコンポーネントです。
useState を利用してプロジェクト データと更新を処理します。
UI と機能のために Header コンポーネントと DragDropArrange コンポーネントが組み込まれています。
通知用に、react-hot-toast のトースターが含まれています。
キー機能:
状態管理: プロジェクト データの状態を管理します。
新しいデータの処理: プロジェクトのデータ状態に新しいデータを追加する関数。
レイアウト: ヘッダー、メインコンテンツ、通知を含むレイアウトを設定します。
import React, { useState } from 'react'; import { Toaster } from 'react-hot-toast'; import Header from './screens/Navigation/Header'; import DragDropArrange from './screens/DDA/DragDropArrange'; import projectDataJson from "./Data/data.json" function App() { const [projectData, setProjectData] = useState(projectDataJson) function handleNewData(data){ const tempData = projectData.newIdea; const maxNumber = (Math.random() * 100) * 1000; tempData.push({_id: maxNumber, idea: data}); setProjectData({...data, newIdea: tempData}) } return ( <div className="h-auto overflow-auto"> <div className='w-full h-auto overflow-auto fixed z-50'> <Header handleNewData={handleNewData}/> </div> <div className="h-auto overflow-auto my-16 flex items-center justify-center"> <DragDropArrange projectData={projectData}/> </div> <Toaster toastOptions={{ className: 'text-xs', duration: 3000, }} /> </div> ); } export default App;
Header.jsx ファイルはナビゲーション バーとして機能し、新しい項目を追加するためのフォームを開くボタンを提供します。
概要:
ナビゲーションと項目入力フォームを開くボタンが含まれています。
useState を使用して、項目入力フォームの可視性の状態を管理します。
新しい項目を追加するためのユーザー操作を処理します。
キー機能:
アイテム フォームの切り替え: アイテム入力フォームの表示/非表示を管理します。
新しいデータの処理: 新しい項目データを親コンポーネントに渡します。
import React, { useState } from 'react'; import { PiNotepadFill } from "react-icons/pi"; import AddIdea from '../DDA/AddIdea'; const Header = ({handleNewData}) => { const [ideaTabOpen, setIdeaTabOpen] = useState(false) return ( <> <nav className='w-full h-auto p-2 float-left overflow-auto bg-black flex items-center justify-center'> <div className='w-2/5 mdw-3/5 lg:w-4/5 h-auto px-6'> <span className='font-bold text-white text-xl lg:text-2xl flex items-center justify-start'><PiNotepadFill/> DDA</span> </div> <div className='w-3/5 md:w-2/5 lg:w-1/5 h-auto flex items-center justify-end px-4'> <button className='text-sm lg:text-lg font-bold text-white border p-2 rounded-lg hover:bg-white hover:text-black border-white active:bg-gray' onClick={() => setIdeaTabOpen(!ideaTabOpen)}>{ideaTabOpen ? "Cancel" : "New Idea"}</button> </div> </nav> {ideaTabOpen && ( <div className='float-left overflow-auto relative w-full'> <AddIdea handleNewData={handleNewData} setIdeaTabOpen={setIdeaTabOpen}/> </div> )} </> ) } export default Header
AddIdea.jsx ファイルは、検証および送信機能を含む、新しい項目を追加するためのフォームを提供します。
概要:
リストに新しい項目を追加するためのコンポーネント。
useState を使用してフォーム入力データと文字数を管理します。
入力の長さを検証し、新しいデータを親コンポーネントに送信します。
主な機能:
ハンドル変更: フォーム入力と文字数を管理します。
送信ハンドル: フォーム データを検証し、親コンポーネントに送信します。
import React, { useState } from 'react'; import toast from "react-hot-toast"; import { Helmet } from 'react-helmet'; const AddIdea = ({ handleNewData, setIdeaTabOpen }) => { const maxLengths = 100; const [formData, setFormData] = useState(); const [remainingChars, setRemainingChars] = useState(80) const handleChange = (e) => { if (e.target.value.length > maxLengths) { toast.error(`${`Input can't be more than ${maxLengths} characters`}`); } else { setFormData(e.target.value); setRemainingChars(maxLengths - e.target.value.length); } }; const handleSubmit = (e) => { e.preventDefault(); if (!formData) { toast.error(`You don't have an idea.`); return } handleNewData(formData); setIdeaTabOpen(false) }; return ( <section className='h-screen'> <div className='flex items-center justify-center bg-blue rounded-b-xl border-b-[10px] py-8 lg:p-10'> <div className='m-1 w-full h-auto flex flex-wrap items-center justify-center'> <div className='sm:w-[90%] w-3/5 h-auto'> <textarea className='w-full h-auto overflow-auto outline-none border p-2 sm:text-sm ' placeholder='Items...' name="items" value={formData} maxLength={maxLengths} onChange={handleChange} ></textarea> <p className={` text-xs ${remainingChars < 15 ? "text-red" : "text-white"}`}>{remainingChars} characters remaining</p> </div> <div className='sm:w-4/5 w-1/5 p-2 flex items-center justify-center'> <button className='bg-primary_button text-white px-4 py-1 rounded-md font-bold' onClick={handleSubmit}>Submit</button> </div> <Helmet><title>Drag Drop & Arrange | New Idea</title></Helmet> </div> </div> <div className='w-full h-full fixed backdrop-blur-[1px]' onClick={()=>setIdeaTabOpen(false)}></div> </section> ); }; export default AddIdea;
DragDropArrange.jsx ファイルは、ドラッグ アンド ドロップ機能を管理し、ユーザー インタラクションに基づいて項目の順序を更新します。
Summary:
Main component for handling drag-and-drop functionality.
Uses DndContext and SortableContext from @dnd-kit/core for drag-and-drop behavior.
Manages state for the data array and updates the order of items based on drag events.
Fetches initial data from projectData and sets it to the state.
Key Functions:
Handle Drag End: Manages the logic for rearranging items based on drag-and-drop actions.
Fetch Data: Fetches initial data and sets it to the component state.
import React, { useState, useEffect } from 'react'; import { DndContext, closestCenter } from '@dnd-kit/core'; import { arrayMove, SortableContext } from '@dnd-kit/sortable'; import { Helmet } from 'react-helmet'; import Arrange from './Arrange'; import Loader from '../Navigation/Loader'; const DragDropArrange = ({projectData}) => { const [dataArray, setDataArray] = useState({ newIdea: undefined, oldIdea: undefined, updateValue: [] }); const handleDragEnd = ({ active, over }) => { if (!over) return; const { fromList, targetId } = active.data.current; const { toList, index } = over.data.current; if (fromList === toList) { const sortedItems = arrayMove(dataArray[toList], dataArray[toList].findIndex((idea) => idea._id === targetId), index); setDataArray((prev) => ({ ...prev, [toList]: sortedItems })); } else { const draggedItem = dataArray[fromList].find((idea) => idea._id === targetId); const updatedFromList = dataArray[fromList].filter((idea) => idea._id !== targetId); const updatedToList = [...dataArray[toList].slice(0, index), draggedItem, ...dataArray[toList].slice(index)]; setDataArray((prev) => ({ ...prev, [fromList]: updatedFromList, [toList]: updatedToList })); } }; const fetchData = async () => { const { newIdea, oldIdea } = projectData; setTimeout(() => { setDataArray((prev) => ({ ...prev, newIdea, oldIdea })); }, 500); }; useEffect(() => { fetchData(); }, []); return ( <section className='w-full h-auto md:w-11/12 lg:w-10/12 py-12 md:p-4 lg:p-12'> <div className='w-full h-auto my-2 overflow-auto'> <div className='w-full h-auto my-4 overflow-auto'> {dataArray.newIdea && dataArray.oldIdea && ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <SortableContext items={dataArray?.newIdea}> <Arrange dataArray={dataArray} /> </SortableContext> </DndContext> )} {!dataArray.newIdea && !dataArray.oldIdea && ( <> <div className='w-full h-auto flex items-center justify-center'><Loader/></div> <div className='w-full text-center text-xl font-bold'> <span className='text-gradient'>Loading...</span> </div> </> )} </div> </div> <Helmet><title>Drag Drop & Arrange | Home</title></Helmet> </section> ); }; export default DragDropArrange
The Arrange.jsx file handles the arrangement of new and old ideas, displaying them in sortable contexts.
Summary:
Uses SortableContext for sortable behavior.
Displays items and manages their order within each category.
Key Functions:
Display Items: Renders items in their respective categories.
Handle Sorting: Manages the sortable behavior of items.
import React from 'react'; import { SortableContext } from '@dnd-kit/sortable'; import Drag from "./Drag"; import Drop from "./Drop"; import Lottie from 'react-lottie'; import NoData from '../../Lottie/NoData.json'; const Arrange = ({ dataArray }) => { const { newIdea, oldIdea } = dataArray; const defaultOptions = { loop: true, autoplay: true, animationData: NoData, rendererSettings: { preserveAspectRatio: "xMidYMid slice" } }; return ( <section className='w-full h-auto rounded-md overflow-auto'> <div className='h-auto overflow-auto flex flex-wrap items-start justify-around'> <div className='w-[48%] min-h-80 h-auto border border-blue shadow-md shadow-white-input-light rounded-md'> <h2 className='bg-blue text-white text-xl text-center font-extrabold py-2'>New Idea</h2> <SortableContext items={newIdea.map(item => item._id)}> {newIdea.length > 0 && ( <> {newIdea?.map((data) => ( <React.Fragment key={data._id}> <Drag data={data} listType="newIdea"/> </React.Fragment> ))} <Drop index={newIdea.length} listType="newIdea" /> </> )} {newIdea.length < 1 && ( <> <div className='w-full h-52 flex items-center justify-center'> <Lottie options={defaultOptions} height={150} width={150} /> </div> <Drop index={newIdea.length} listType="newIdea" /> </> )} </SortableContext> </div> <div className='w-[48%] min-h-80 h-auto border border-blue shadow-md shadow-white-input-light rounded-md'> <h2 className='bg-blue text-white text-xl text-center font-extrabold py-2'>Old Idea</h2> <SortableContext items={oldIdea.map(item => item._id)}> {oldIdea.length > 0 && ( <> {oldIdea?.map((data) => ( <React.Fragment key={data._id}> <Drag data={data} listType="oldIdea" /> </React.Fragment> ))} <Drop index={oldIdea.length} listType="oldIdea" /> </> )} {oldIdea.length < 1 && ( <> <div className='w-full h-52 flex items-center justify-center'> <Lottie options={defaultOptions} height={150} width={150} /> </div> <Drop index={oldIdea.length} listType="oldIdea" /> </> )} </SortableContext> </div> </div> </section> ); } export default Arrange
The Drag.jsx file manages the draggable items, defining their behavior and style during the drag operation.
Summary:
Manages the behavior and style of draggable items.
Uses useDraggable from @dnd-kit/core for draggable behavior.
Defines the drag and drop interaction for items.
Key Functions:
useDraggable: Provides drag-and-drop functionality.
Style Management: Updates the style based on drag state.
import React from 'react'; import { useDraggable } from '@dnd-kit/core'; import { IoMdMove } from "react-icons/io"; const Drag = ({ data, listType }) => { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: data._id, data: { fromList: listType, targetId: data._id }, }); const style = { transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined, opacity: isDragging ? 0.5 : 1, pointerEvents: isDragging ? 'none' : 'auto', }; return ( <> <section className="w-auto h-auto bg-yellow rounded-md border overflow-hidden m-2" ref={setNodeRef} style={style} {...listeners} {...attributes} > <div> <div className="w-auto p-2 h-auto bg-yellow"> <p className="h-auto text-xs lg:text-sm text-black break-all">{data?.idea}</p> </div> </div> </section> </> ); }; export default Drag;
The Drop.jsx file defines the droppable areas where items can be dropped, including visual feedback during the drag operation.
Summary:
Uses useDroppable from @dnd-kit/core for droppable behavior.
Provides visual feedback during drag-and-drop interactions.
Key Functions:
useDroppable: Provides droppable functionality.
Handle Drop: Manages drop actions and updates the state accordingly.
import React from 'react'; import { useDroppable } from '@dnd-kit/core'; const Drop= ({ index, setDragged, listType }) => { const { isOver, setNodeRef } = useDroppable({ id: `${listType}-${index}`, data: { toList: listType, index }, }); const handleDrop = (e) => { e.preventDefault(); setDragged({ toList: listType, index }); }; return ( <section ref={setNodeRef} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()} className={`w-auto h-16 rounded-lg flex items-center justify-center text-xs text-secondary_shadow ${isOver ? ` opacity-100` : `opacity-0`}`} style={{ pointerEvents: 'none' }} > </section> ); }; export default Drop
By following this comprehensive guide, you can create a dynamic and user-friendly drag-and-drop interface for your applications. This setup not only enhances user experience but also makes managing and organizing items intuitive and efficient. The combination of React, Tailwind CSS, and Dnd-kit provides a robust foundation for building such interactive features.
Feel free to customize and extend this implementation to suit your specific needs. Happy coding!
You can find the complete source code for this project in my GitHub repository:
Github Link
以上がReact、Tailwind CSS、Dnd-kit を使用してアイテムを配置/並べ替えるためのドラッグ アンド ドロップを実装するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。