本系列文以製作專案為主軸,紀錄小弟學習React以及GrahQL的過程。主要是記下重點步驟以及我覺得需要記憶的部分,有覺得不明確的地方還請留言多多指教。
React DnD是用來在React中實作拖曳功能的(廢話),可以做到蠻詳細的訂製,而且可以用耦合性低的方式加裝在既有的component上。
這個套件原生建立在HTML5 Drag and Drop API上,這導致他無法對應手機、平板的觸控拖曳功能,不過別擔心,HTML5 API是可替換的,可以把HTML backend替換成Touch backend以對應觸控拖曳。
當用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>
);
}
//...
前面有提到"拖曳帶有某屬性的物件種類(type)",在DnD當中必須明確拖曳的是什麼type,而接受拖放(drop)的物件可以設定只接收某種type,如果拖曳的type不符合的話拖放(drop)就會失敗。
我這邊另外開一個資料夾dnd來放這個type的設定
/**
src/dnd/constants.js
**/
export const ItemTypes = {
TODO: "todo",
};
目前先制定TODO這個type。
要將一個物件定義為可拖曳,用的是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來進行比對時就能確保一致性。
當拖曳一個物件時,除了更新拖曳物件的item外,其實背後也更新了許多狀態屬性,像是 isDragging,用於紀錄物件是否處於被拖曳的狀態。
而存取這些狀態的窗口,就是React DnD的monitor。
monitor可以從很多地方存取,以下用useDrag的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}` }