iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
Modern Web

30天打造個人簡易旅遊網站系列 第 25

Day 25:Draggable-拖曳畫面中的物件

  • 分享至 

  • xImage
  •  

今天要來實做拖曳畫面中的物件,原本我們預計是使用React-beautiful-dnd這個套件,但由於現在React 18不支援strict mode,拖曳中會出現找不到id的問題,那我們在這幾天發現用@hello-pangea/dnd這個套件可以完美解決問題,只需將import的來源改成這個,其他部分跟React-beautiful-dnd一模一樣。那首先一樣先透過npm install @hello-pangea/dnd 來安裝。安裝完後,就一步步來完成這頁吧!

1.新增ScheduleDetailPage:

在建立的行程清單中再點進去一層,就會到這個頁面,為了排版與其他頁都一致所以也要在這邊建立這頁,還有Route也記得新增一條路徑”<Route path="/schedule/:scheduleId" element={} />” 那在這裡就先略過。

import Header from "../../components/Header/Header";
import ScheduleDetail from "../../components/ScheduleDetail";
import styles from "./scheduleDetail.module.css";
import { Row, Col } from "antd";

export default function ScheduleDetailPage() {
  return (
    <>
      <div className={styles.container}>
        <Row className={styles.row}>
          <Col
            sm={{ span: 24 }}
            md={{ span: 24 }}
            lg={{ span: 4 }}
            className={styles.col_4}
          >
            <Header />
          </Col>
          <Col
            sm={{ span: 24 }}
            md={{ span: 24 }}
            lg={{ span: 16 }}
            className={styles.col_16}
          >
            <ScheduleDetail />
          </Col>
        </Row>
      </div>
    </>
  );
}

2.從ScheduleList傳入資料:

老樣子,一步步將資料傳到下一層的元件中。

export default function ScheduleList() {
  const selectSchedule = useSelector(selectScheduleName) || [];
  return (
    <>
      <div className={styles.containerbox}>
        <h1 className={styles.title}>Your Schedule List</h1>
        <Row className={styles.container}>
          {selectSchedule.map((schedule) => (

            <Col span={24} className={styles.col}>
              <ScheduleItem key={schedule.scheduleId} schedule={schedule} />
            </Col>
          ))}

        </Row>
      </div>
    </>
  );
}

這邊要注意一下,要透過 new Date(schedule.time[0])new Date(schedule.time[1])的方式來確保這些日期的數值被轉換為 Date 物件,這樣可以安全地調用日期方法。如果 schedule.time 中的某個值是 undefined 或無效的日期字串,直接調用日期方法會導致錯誤。

export default function ScheduleItem({ schedule }) {
  const startDate = new Date(schedule.time[0]);
  const endDate = new Date(schedule.time[1]);
  const startTime = `${startDate.getMonth() + 1}/${startDate.getDate()}`;
  const endTime = `${endDate.getMonth() + 1}/${endDate.getDate()}`;
  return (
    <>
      <Link to={`/schedule/${schedule.scheduleId}`}>
        <div className={styles.favoritebox}>
          <div className={styles.placebox}>
            <img src={Logo} alt="logo" width={"40px"} />
            <h2 className={styles.name}>{schedule.scheduleName}</h2>
          </div>
          <div>
            <h2 className={styles.date}>{startTime}~{endTime}</h2>
          </div>
        </div>
      </Link>
    </>
  );
}

3.更新Redux架構:

由於每次利用拖曳來更新順序位置,也都要在redux做更新,所以也要新增updateLandmarkOrder這個action來處理這個問題。

reducer:{
	updateLandmarkOrder: (state, action) => {
	    const { scheduleName, newLandmarks } = action.payload;
	    const scheduleIndex = state.schedules.findIndex(
                    schedule => schedule.scheduleName === scheduleName);
	    if (scheduleIndex >= 0) {
	        state.schedules[scheduleIndex].landmarks = newLandmarks;
	    }
	}
}

export const { updateLandmarkOrder } = addToScheduleSlice.actions;

4.在ScheduleDetail實現拖曳功能:

一開始先匯入會用到的套件

import React from 'react';
import { useSelector ,useDispatch} from 'react-redux';
import { useParams } from 'react-router-dom';
import { Row, Col } from 'antd';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
import styles from './schedule.module.css';
import { selectScheduleName, updateLandmarkOrder } from '../redux/addToSchedule';

再來定義好相關的變數:

  • scheduleId: 從 URL 路徑中提取的行程 ID。
  • schedules: 從 Redux store 中獲取所有行程的列表。
  • dispatch: 用於發送動作到 Redux store。
  • schedule: 根據 scheduleId 在行程列表中查找對應的行程。如果找不到,會顯示 "行程不存在"。
  • startDateendDate: 將行程的開始和結束時間轉換為 Date 對象。
  • startTimeendTime: 將日期格式化為 月/日 的格式。
const { scheduleId } = useParams(); // 從路由中獲取行程 ID
const schedules = useSelector(selectScheduleName);
const dispatch = useDispatch();
const schedule = schedules.find(s => s.scheduleId === scheduleId); // 根據 ID 獲取行程

const startDate = new Date(schedule.time[0]);
const endDate = new Date(schedule.time[1]);
const startTime = `${startDate.getMonth() + 1}/${startDate.getDate()}`;
const endTime = `${endDate.getMonth() + 1}/${endDate.getDate()}`;

處理拖曳事件:

  • result.destination: 如果目標位置不存在(例如將項目拖到無效區域),則不進行任何操作。
  • items: 創建行程中景點的副本。
  • reorderedItem: 從原始位置移除的景點。
  • splice: 將景點插入到新的目標位置。
  • dispatch: 發送 updateLandmarkOrder 動作,將新的景點順序更新到 Redux store。
const handleOnDragEnd = (result) => {
    if (!result.destination) return;

    const items = Array.from(schedule.landmarks);
    const [reorderedItem] = items.splice(result.source.index, 1);
    items.splice(result.destination.index, 0, reorderedItem);

		dispatch(updateLandmarkOrder({ 
				scheduleName: schedule.scheduleName, newLandmarks: items }));
}
  • DragDropContext: 提供拖放上下文,並設置 onDragEnd 回調。
  • Droppable: 定義一個可放置的區域,droppableId"landmark"
  • provided.droppablePropsref: 用於將 Droppable 區域連接到拖放系統。
  • provided.placeholder: 占位符,用於保留拖放期間的空間。
<DragDropContext onDragEnd={handleOnDragEnd}>
    <Droppable droppableId="landmark">
        {(provided) => (
            <div
                className={styles.favoritebox}
                {...provided.droppableProps}
                ref={provided.innerRef}
            >
                {/* Draggable 列表 */}
                {provided.placeholder}
            </div>
        )}
    </Droppable>
</DragDropContext>
  • Draggable: 每個景點被包裹在 Draggable 組件中,允許其被拖曳。
  • key: React 的 key 屬性,使用景點的 id 保證唯一性。如果沒有 id,則使用 nameindex 的組合(不建議,但此處有備選方案)。
  • draggableId: 必須是唯一的字符串,用於識別拖曳項目。這裡使用景點的 id 轉換為字符串。
  • index: 在列表中的索引,用於拖放位置的計算。
  • provided.draggablePropsprovided.dragHandleProps: 用於將 Draggable 組件連接到拖放系統。
  • style: 合併拖放系統提供的樣式,並設置滑鼠指標為 move,提示用戶可以拖曳。
{schedule.landmarks.map((landmark, index) => (
    <Draggable 
        key={landmark.id || `${landmark.name}-${index}`} 
        draggableId={landmark.id ? landmark.id.toString() : `${landmark.name}-${index}`} 
        index={index}
    >
        {(provided) => (
            <div
                className={styles.placebox}
                ref={provided.innerRef}
                {...provided.draggableProps}
                {...provided.dragHandleProps}
                style={{
                    ...provided.draggableProps.style,
                    cursor: 'move',
                }}
            >
                <img src={landmark.image} alt={landmark.name} width="150px" />
                <h2 className={styles.name}>{landmark.name}</h2>
            </div>
        )}
    </Draggable>
))}

重點總結:

  • 唯一的 draggableIdkey:使用每個景點的 id 作為 draggableIdkey,確保在拖放過程中能夠正確識別和管理每個景點。
  • DragDropContext 和 Droppable:這些組件設置了拖放的上下文和可放置區域,確保拖放功能正常運作。
  • Draggable:每個景點被包裹在 Draggable 組件中,允許其被拖曳並重新排列。
  • Redux:通過 useSelector 獲取行程數據,並通過 dispatch 發送動作來更新景點順序,確保全局狀態與 UI 同步。

5.Droppable的完整程式碼範例:

<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="landmark">
    {(provided) => (
        <div
            className={styles.favoritebox}
            {...provided.droppableProps}
            ref={provided.innerRef}
        >
            {schedule.landmarks.map((landmark, index) => (
                <Draggable 
                    key={landmark.id || `${landmark.name}-${index}`} 
                    draggableId={landmark.id ? landmark.id.toString() : 
                                                `${landmark.name}-${index}`} 
                    index={index}
                >
                    {(provided) => (
                        <div
                            className={styles.placebox}
                            ref={provided.innerRef}
                            {...provided.draggableProps}
                            {...provided.dragHandleProps}
                            style={{
                                ...provided.draggableProps.style,
                                cursor: 'move',
                            }}
                        >
                            <img src={landmark.image} alt={landmark.name} width="150px" />
                            <h2 className={styles.name}>{landmark.name}</h2>
                        </div>
                    )}
                </Draggable>
            ))}
            {provided.placeholder}
        </div>
    )}
</Droppable>
</DragDropContext>

6.總結:

經過今天的教學,可以發現其實拖曳功能並不是很難,只是因為原本React-beautiful-dnd套件會有bug才多讓筆者花了點時間去尋找解決方案。這功能對我們的專案來說是滿實用的,因為旅遊景點的順序隨時可能會做更換。希望大家會喜歡今天的教學!實際畫面在這裡:draggable實際畫面


上一篇
Day 24:使用Redux建立一個旅遊行程(三)
下一篇
Day 26:利用redux-persist暫存瀏覽器資料
系列文
30天打造個人簡易旅遊網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言