iT邦幫忙

2023 iThome 鐵人賽

DAY 14
1
SideProject30

營養師不開菜單要用 Next.js 13 寫全端系列 第 14

營養師不開菜單的第十四天 - 為什麼要用 React-Beautiful-Dnd 做拖曳效果

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20230928/20152073liznyeX2ag.png

React-Beautiful-DnD 是由 Atlassian 開發,也就是 Trello、Slack、Jira 的母公司,但這邊要澄清一下,雖然 Trello 擁有使用者體驗很好的拖曳功能,但 Trello 是 2017 年被 Atlassian 收購,所以不是用 React-Beautiful-DnD 開發的喔!回到 React-Beautiful-DnD ,它提供 React 開發者更簡易的實現列表拖曳功能,主要使用 CSS 和 React實作動畫及交互作用,在美觀及功能上都提升開發者體驗。

為什麼要使用 react-beautiful-dnd


  1. 簡單的 API:只需要調用套件封裝好的 component 再寫入自訂義的邏輯即可完成功能。
  2. 無障礙設計:支持鍵盤控制並與輔助技術工具協作可達到元件的無障礙性
  3. 現成美觀動畫:提供預設平滑及順暢的拖曳動畫,可以提升使用者體驗。
  4. 可客製化性:雖然預設提供的功能就可以完成大部分的需求,但 React-Beautiful-DnD 依舊提供可製化的功能,例如改變正在被拖曳元件的樣式或

提供的功能


  • <DragDropContext /> - 想要進行 drag and drop 的範圍,<Droppable /><Draggable /> 的 wrapper,類似 provider 的功能
  • <Droppable /> - 可執行 drop 的範圍。 可包含許多  <Draggable />
  • <Draggable /> - 可被拖移的原件範圍
  • resetServerContext() - 提供給 SSR 使用

<DragDropContext />


handler

  • onDragEnd - 必要屬性
  • onDragStart - 拖曳行為開始時觸發
  • onDragUpdate - 拖曳行為讓順序產生變動時觸發
  • onBeforeCapture - 在拖動操作即將開始之前,尚未從網頁的 DOM 中獲取相關元素的尺寸資訊
  • onBeforeDragStart - 在拖動操作即將開始之前,已從網頁的 DOM 中獲取相關元素的尺寸資訊

props

  • children
  • dragHandleUsageInstructions - 當焦點聚焦在拖曳 handler 上時,透過螢幕閱讀器朗讀出來的內容。
  • nonce - 用於嚴格的內容安全策略
  • sensors - 提供客製化的 sensor 設置
  • enableDefaultSensors

onDragEnd

套件中主要的操作功能,可以在這個 function 中處理拖曳後的結果,因為會接收一個拖曳後的列表資訊,例如哪個元素被拖動、它原本在哪裡、被放到了哪裡等等。

屬性:

  • source - 被拖曳 item 的 droppableId 及原本的 index
  • destination - 被拖曳 item 的 droppableId 及目的地位置的 index
  • combine - 當 <Droppable/>isCombineEnabled 屬性被設為 true 時可調用的 flag

<Droppable />


希望使用者可以將元件拖拽到哪個位置範圍,可以使用 <Droppable />

為一個頭尾標籤,其中必填屬性為 droppableId 及一個包含 providedsnapshot 參數的 children。

範例:

import { Droppable } from 'react-beautiful-dnd';

<Droppable droppableId="droppable-1" type="PERSON">
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
      {...provided.droppableProps}
    >
      <h2>I am a droppable!</h2>
      {provided.placeholder}
    </div>
  )}
</Droppable>;

props

droppableId - 必填屬性,為識別 Droppable component 的唯一 Id

children

包含 providedsnapshot 參數,並回傳一個 reactElement

  1. provided
    1. provided.innerRef - 必設,設定給 reactElement 的 ref
    2. provided.droppableProps - 以解構方式設定於 reactElement,用於樣式和查找的資料屬性
    3. provided.placeholder - 使拖曳時有保留該元素的空間
  2. snapshot - 多使用於樣式設定
    1. isDraggingOver - 判斷 <Droppable /> 中的 <Draggable /> 元素是否正在被拖曳
    2. draggingOverWith - 正在 <Droppable /> 上被拖曳的元素 id
    3. draggingFromThisWith - 在列表中拖曳的元素 id ,可針對尚未進行拖曳時的列表做樣式設定。
    4. isUsingPlaceholder - 在 virtual 模式時,可判斷 placeholder 是否正在被使用。

<Draggable />


Draggable 組件代表了可以被拖拽的項目,必須被包在 Droppable 的 children 中。

<Droppable droppableId="droppable-1">
  {(provided, snapshot) => (
    <div ref={provided.innerRef} {...provided.droppableProps}>
      <Draggable key={item?.id} draggableId={item?.id} index={index}>
        {(provided) => (
          <div
            ref={provided.innerRef}
            {...provided.draggableProps}
            {...provided.dragHandleProps}
          >
            <MdDragIndicator size={24} className="text-grey-400" />
            <DisplayLinkItem />
          </div>
        )}
      </Draggable>
      {provided.placeholder}
    </div>
  )}
</Droppable>;

為一個頭尾標籤,其中必填屬性為 draggableIdindex 及一個包含 providedsnapshot 參數的 children。

props

  • draggableId - 必填屬性,為識別各個 Draggable component 的唯一 Id
  • index - 必填屬性,onDragEnd 調用時的列表順序判斷依據
  • key - React Element 必填屬性

children

包含 providedsnapshot 參數,並回傳一個 React Element

provided

  1. provided.innerRef - 必設,設定給各個 React Element 的 ref
  2. provided.droppableProps - 以解構方式設定於 reactElement,用於樣式和查找的資料屬性
    • style - droppable 組件的樣式
    • onTransitionEnd
  3. provided.dragHandleProps - 以解構方式設定於執行拖曳行為的物件上,包含 tabIndexdraggableonDragStart 屬性
<div
	ref={provided.innerRef}
  {...provided.draggableProps} // 被拖曳的組件
  style={getItemStyle(
		snapshot.isDragging,
    provided.draggableProps.style
  )}
>
  <div {...provided.dragHandleProps}> // 只有在這個元素上可執行拖曳動作
     <DragHandleIcon />
  </div>

	<ScheduleItem
    index={index}
    formik={formik}
    handleDelete={handleDelete}
	/>
</div>

https://ithelp.ithome.com.tw/upload/images/20230928/20152073RjswiBoGIL.png

snapshot - 多使用於樣式設定

  1. isDragging - 判斷該 Draggable 元素是否正在被拖曳
  2. isDropAnimating - 判斷是否正在進行拖曳動畫 (基本上用不到的屬性)
  3. dropAnimation - 當前被拖拽的元素正在懸停或拖曳至哪個放置目標,通常為 Droppable 的id
  4. draggingOver - 當一個 Draggable 被放下時的動畫效果

成果範例

img

版本相容問題


在 React 18 的 Strict mode 下, useLayoutEffect 的行為會與之前的版本有所不同,特別是在 concurrent rendering 的環境下。react-beautiful-dnd 是在 useLayoutEffect 中註冊它的 droppables,如下範例:

useLayoutEffect(() => {
    registry.draggable.register(publishedRef.current);
    return () => registry.draggable.unregister(publishedRef.current);
  }, [registry.draggable]);

依據 dependency array 中的設定,雖然在初次的 mount 進行時會觸發此 useLayoutEffect,但當元件在 StrictMode 下因 concurrent rendering 而進行 remount 操作時,useLayoutEffect 可能不會被再次觸發。這種行為可能導致 Droppable 和 Draggable 無法被完全渲染,並出現 unable to find draggable with id: 錯誤。

因此在 Strict mode 下的解決方法就是使用 useEffect 及 requestAnimationFrame,useEffect 是為了確保元件在瀏覽器渲染畫面後再渲染,requestAnimationFrame 是為了使其中的 callback function 在下一次瀏覽器重繪之前被執行,使得 Droppable 和 Draggable 被正確的渲染。

import { useEffect, useState } from "react";
import { Droppable, DroppableProps } from "react-beautiful-dnd";

export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
  const [enabled, setEnabled] = useState(false);

  useEffect(() => {
    const animation = requestAnimationFrame(() => setEnabled(true));

    return () => {
      cancelAnimationFrame(animation); // 組件卸載時取消請求的動畫幀,以防止內存洩漏
      setEnabled(false);
    };
  }, []);

  if (!enabled) {
    return null;
  }

  return <Droppable {...props}>{children}</Droppable>;
};

套件維護

兩年前官方已經宣布除了安全性的修正,現階段不會再進行其他維護更新,所以在日新月異的框架技術中會出現越來越多 issue,但基於方便性以及美觀性,使用者還是非常的多。為了解決上述的問題,社群上有熱心的開發者直接 fork 了原專案進行維護及修正,也將套件以 TypeScript 進行重構 - hello-pangea/dnd,使用方法跟原本的 React-Beautiful-DnD 一模一樣,只需要在 import 時改為 @hello-pangea/dnd,並且也修正了上面 React 18 的 issue。提供這個資訊給還是希望使用 React-Beautiful-DnD 但又因版本問題深陷苦惱的各位。

https://ithelp.ithome.com.tw/upload/images/20230928/20152073zC0m01jNc9.png

參考資料

https://github.com/atlassian/react-beautiful-dnd/blob/013bfceac04ff48548c33cdc468dd2927446fc1b/src/view/use-draggable-publisher/use-draggable-publisher.js#L71

https://github.com/atlassian/react-beautiful-dnd/issues/2399#issuecomment-1137880007

https://github.com/hello-pangea/dnd

https://ithelp.ithome.com.tw/upload/images/20230928/20152073NSqLnOYSJk.png


上一篇
營養師不開菜單的第十三天 - 不需要 React Provider 管理狀態的 Zustand
下一篇
營養師不開菜單的第十五天 - 為什麼從 Formik 跳槽到 React-Hook-Form
系列文
營養師不開菜單要用 Next.js 13 寫全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言