iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
1
Modern Web

React + GraphQL 全端練習筆記系列 第 15

仿Trello - 用React DnD製作拖曳(drag)功能

本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。

React DnD是用來在React中實作拖曳功能的(廢話),可以做到蠻詳細的訂製,而且可以用耦合性低的方式加裝在既有的component上。

這個套件原生建立在HTML5 Drag and Drop API上,這導致他無法對應手機、平板的觸控拖曳功能,不過別擔心,HTML5 API是可替換的,可以把HTML backend替換成Touch backend以對應觸控拖曳。

狀態(state)驅動

當用React DnD實作拖曳的時候,他並不是在"拖曳某個dom",而是在開始拖曳的同時向React DnD的狀態庫更新"拖曳帶有某屬性的物件種類(type)",然後產生拖曳的畫面,接者在drop後根據新的state更新畫面。

這跟React是一脈相承的思路,先更新state,再根據新的state重新渲染畫面。

詳細的過程在接下來的實作中根據範例說明會較明確,先記著狀態驅動的概念就好。

安裝

指令:

npm i react-dnd react-dnd-html5-backend

上面提過拖曳的功能是基於HTML5的API製作,
所以安裝時要連帶 html5 backend 一起安裝。

安裝完成後要制定可以使用DnD功能的範圍:

//App.js

//...
//加入DnD功能
import { DndProvider } from "react-dnd";  
import { HTML5Backend } from "react-dnd-html5-backend";

function App() {
  return (
    <DndProvider backend={HTML5Backend}> //可存取DnD功能的Context,必須指定使用的backend
      <div className="App ">
        <KanBan></KanBan>
      </div>
    </DndProvider>
  );
}

//...

ItemTypes

前面有提到"拖曳帶有某屬性的物件種類(type)",在DnD當中必須明確拖曳的是什麼type,而接受拖放(drop)的物件可以設定只接收某種type,如果拖曳的type不符合的話拖放(drop)就會失敗。

我這邊另外開一個資料夾dnd來放這個type的設定

/**
    src/dnd/constants.js
**/
export const ItemTypes = {
  TODO: "todo",
};

目前先制定TODO這個type。

useDrag

要將一個物件定義為可拖曳,用的是useDrag這個Hook。

準備把Todo做成可拖曳:

// Todo.jsx
//...
import { useDrag } from "react-dnd";
import { ItemTypes } from "../dnd/constants.js";

export default function Todo({ name, listId, todoId, updateEditState }) {

  //...
 
  //用useDrag定義拖曳行為
  const [ , drag] = useDrag({
    item: { listId, todoId, type: ItemTypes.TODO }
  });
  
  //...
}

useDrag回傳的是一個陣列,陣列的第一個元素先空著,待會會用到。

第二個元素 drag,用來指定要成為可拖曳物件的element,一般用ref綁定。

// Todo.jsx
//...

export default function Todo({ name, listId, todoId, updateEditState }) {

  //...

  const [ , drag] = useDrag({
    item: { listId, todoId, type: ItemTypes.TODO }
  });
  
  return (
    <div ref={drag}> //多用一層div包起來,將drag綁定
      <div
        className="todo text-wrap my-1 p-2 rounded"
        ref={targetRef}
        onMouseEnter={handleOnOver}
        onMouseLeave={handleOnLeave}
      >
        {name}
        {isOver && (
          <Button
            className="edit-button m-1"
            size="sm"
            onClick={handelClickEdit}
          >
            <FontAwesomeIcon icon={faPencilAlt} />
          </Button>
        )}
      </div>
    </div>
  );
}

不過像這裡Todo裡已經有targetRef的狀況下,可以改用 drag(targetRef) 這個方法:

// Todo.jsx
//...

export default function Todo({ name, listId, todoId, updateEditState }) {

  //...

  const [ , drag] = useDrag({
    item: { listId, todoId, type: ItemTypes.TODO }
  });
  
  //套用drag ref到既有的ref上
  drag(targetRef); 
  
  return (
      <div
        className="todo text-wrap my-1 p-2 rounded"
        ref={targetRef}
        onMouseEnter={handleOnOver}
        onMouseLeave={handleOnLeave}
      >
        {name}
        {isOver && (
          <Button
            className="edit-button m-1"
            size="sm"
            onClick={handelClickEdit}
          >
            <FontAwesomeIcon icon={faPencilAlt} />
          </Button>
        )}
      </div>
  );
}

到這邊所有的Todo已經可以被拖出來了。

不過只是產生一個跟著滑鼠跑的半透明複製品而已,一放開就打回原形,因為我們還沒實際對todos的資料做改動。

而當你開始拖曳的時候,會通知React DnD的狀態庫你正在拖曳的 item屬性:

 item: { listId, todoId, type: ItemTypes.TODO }

item的設定對useDrag是必要的,而item中必須帶有type屬性,宣告這個拖曳物件的種類,這裡從之前製作的constants.js裡取得 ItemTypes.TODO。

把type制定在外部檔案中,之後在drop時也從constants.js中取得type來進行比對時就能確保一致性。

monitor

當拖曳一個物件時,除了更新拖曳物件的item外,其實背後也更新了許多狀態屬性,像是 isDragging,用於紀錄物件是否處於被拖曳的狀態。

而存取這些狀態的窗口,就是React DnD的monitor。

monitor可以從很多地方存取,以下用useDrag的collect為例。

collect

  const [ , drag] = useDrag({
    item: { listId, todoId, type: ItemTypes.TODO }
  });

剛剛useDrag回傳的陣列中只用到第二個物件,drag參照。

而陣列的第一個物件,是用於蒐集拖曳狀態的屬性。

範例:

const [{ isDragging }, drag] = useDrag({
    item: {
      orgListId: listId,
      orgTodoId: todoId,
      type: ItemTypes.TODO,
    },
    //蒐集狀態
    collect: (monitor) => ({ 
      isDragging: monitor.isDragging(),
    }),
});

useDrag中的預設屬性collect,會回傳一個Object作為useDrag回傳陣列中的第一個值,而在collect裡可以提取monitor作為參數,從中取得拖曳的狀態。

像是monitor.isDragging(),如果物件正在被拖曳,則值為true。

可以利用這些屬性處理畫面的更新,像是用於class的新增。

以下範例是當isDragging為true時,增加dragged這個class,讓Todo顯示成灰色。

className={`todo text-wrap my-1 p-2 rounded 
${isDragging ? "dragged" : null}` } 

References:


上一篇
仿Trello - 製作reducer
下一篇
仿Trello - 用React DnD製作拖放(drop)功能
系列文
React + GraphQL 全端練習筆記30

尚未有邦友留言

立即登入留言