今天要來實做拖曳畫面中的物件,原本我們預計是使用React-beautiful-dnd這個套件,但由於現在React 18不支援strict mode,拖曳中會出現找不到id的問題,那我們在這幾天發現用@hello-pangea/dnd這個套件可以完美解決問題,只需將import的來源改成這個,其他部分跟React-beautiful-dnd一模一樣。那首先一樣先透過npm install @hello-pangea/dnd 來安裝。安裝完後,就一步步來完成這頁吧!
在建立的行程清單中再點進去一層,就會到這個頁面,為了排版與其他頁都一致所以也要在這邊建立這頁,還有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>
</>
);
}
老樣子,一步步將資料傳到下一層的元件中。
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>
</>
);
}
由於每次利用拖曳來更新順序位置,也都要在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;
一開始先匯入會用到的套件
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
在行程列表中查找對應的行程。如果找不到,會顯示 "行程不存在"。Date
對象。月/日
的格式。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()}`;
處理拖曳事件:
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 }));
}
onDragEnd
回調。droppableId
為 "landmark"
。<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="landmark">
{(provided) => (
<div
className={styles.favoritebox}
{...provided.droppableProps}
ref={provided.innerRef}
>
{/* Draggable 列表 */}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
Draggable
組件中,允許其被拖曳。key
屬性,使用景點的 id
保證唯一性。如果沒有 id
,則使用 name
和 index
的組合(不建議,但此處有備選方案)。id
轉換為字符串。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>
))}
重點總結:
draggableId
和 key
:使用每個景點的 id
作為 draggableId
和 key
,確保在拖放過程中能夠正確識別和管理每個景點。Draggable
組件中,允許其被拖曳並重新排列。useSelector
獲取行程數據,並通過 dispatch
發送動作來更新景點順序,確保全局狀態與 UI 同步。<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>
經過今天的教學,可以發現其實拖曳功能並不是很難,只是因為原本React-beautiful-dnd套件會有bug才多讓筆者花了點時間去尋找解決方案。這功能對我們的專案來說是滿實用的,因為旅遊景點的順序隨時可能會做更換。希望大家會喜歡今天的教學!實際畫面在這裡:draggable實際畫面