首頁  >  文章  >  web前端  >  使用 React、Tailwind CSS 和 Dnd-kit 實作拖放來排列/排序項目

使用 React、Tailwind CSS 和 Dnd-kit 實作拖放來排列/排序項目

PHPz
PHPz原創
2024-07-25 08:23:12530瀏覽

Implementing Drag and Drop to Arrange/Sort Items with React, Tailwind CSS, and Dnd-kit

介紹

您是否想知道 Trello 或 Asana 等應用程式如何管理其直覺的拖放介面?想像一下您有一個應用程序,用戶需要輕鬆地對其項目進行排序。如果沒有流暢的拖放功能,這項任務可能會變得乏味且令人沮喪。在這篇文章中,我們將探索如何使用 React、Tailwind CSS 和 Dnd-kit 實現動態拖放功能,以建立用於排列和排序專案的無縫使用者體驗。

現實世界的問題

在現實世界中,應用程式通常要求使用者根據優先順序、狀態或其他標準重新排列項目。例如,用戶可能需要在腦力激盪會議期間快速重新排序他們的想法。如果沒有高效的拖放功能,此過程可能涉及繁瑣的步驟,例如手動編輯專案位置或使用低效的上移/下移按鈕。我們的目標是提供一種解決方案來簡化此流程,使其對使用者來說更加直觀和高效。

使用案例

讓我們考慮一個腦力激盪工具的用例,使用者可以在其中組織他們的想法。使用者需要具備以下能力:

  • 將新想法加入清單。

  • 透過將這些想法拖放到所需的順序來對它們進行排序和優先順序。

  • 在不同類別之間轉移想法(例如,新想法與舊想法)。

為了實現這一目標,我們將使用 Vite 進行專案設定、使用 Tailwind CSS 進行樣式設定、使用 Dnd-kit 進行拖放功能來建立 React 應用程式。此設定將使我們能夠創建一個用戶友好的介面,從而提高生產力和用戶體驗。

設定項目

  • 初始化Vite專案

npm create vite@latest my-drag-drop-app --template React
cd my-drag-drop-app
npm 安裝

  • 安裝所需的依賴項:

npm install tailwindcss dnd-kit React-hot-toast React-icons

  • 設定 Tailwind CSS:

npx tailwindcss 初始化

  • 更新 tailwind.config.js:
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  ],
  plugins: [],
}
  • 將 Tailwind 指令加入 index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;

實現拖放

應用程式.jsx

App.jsx 檔案是設定應用程式整體佈局並管理全域狀態的主要元件。

總結

  • 管理整個應用程式狀態的主要元件。

  • 利用 useState 處理項目資料和更新。

  • 合併了 UI 和功能的 Header 和 DragDropArrange 元件。

  • 包括來自react-hot-toast的Toaster用於通知。

主要功能:

  • 狀態管理:管理專案資料的狀態。

  • 處理新資料:將新資料加入項目資料狀態的函數。

  • 版面: 設定版面配置,包括標題、主要內容和通知。

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;

標頭.jsx

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

加入創意.jsx

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

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

Arrange.jsx

The Arrange.jsx file handles the arrangement of new and old ideas, displaying them in sortable contexts.

Summary:

  • Manages the arrangement of new and old ideas.
  • 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

Drag.jsx

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;

Drop.jsx

The Drop.jsx file defines the droppable areas where items can be dropped, including visual feedback during the drag operation.

Summary:

  • Manages the behavior of droppable areas.
  • 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 

Conclusion

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!

Source Code

You can find the complete source code for this project in my GitHub repository:
Github Link

以上是使用 React、Tailwind CSS 和 Dnd-kit 實作拖放來排列/排序項目的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn