เมื่อการออกแบบ UI กลายเป็นเรื่องง่ายด้วย Drag to Swap
เคยรู้สึกไหมว่าการจัดเรียงข้อมูลหรือองค์ประกอบบนเว็บไซต์และแอปพลิเคชันเป็นเรื่องยุ่งยาก? วันนี้เราจะมาพาคุณสำรวจเทคนิคสุดล้ำที่จะเปลี่ยนวิธีคิดในการออกแบบอินเตอร์เฟซของคุณ - นั่นก็คือ "Drag to Swap"
เทคนิคนี้ไม่เพียงแต่ทำให้การใช้งานง่ายขึ้น แต่ยังเพิ่มประสบการณ์ผู้ใช้งาน ช่วยเพิ่ม interaction มากขึ้น ด้วยวันนี้เราจะมาแนะนำ dependency ตัวหนึ่งนั้นก็คือ Swapy สามารถใช้ได้ทั้ง
React
Vue
Svelte
โดยในวันนี้จะมาแสดงวิธีการง่ายๆ สำหรับการทำ Drag to Swap บน React (Next.js) กันครับ
อ่านเพิ่มเติมได้ที่
https://swapy.tahazsh.com/docs/installation/ขั้นตอนที่ 1: Install Swapy เข้า Project ของเรา
npm install swapy
หรือ
<script src="https://unpkg.com/swapy/dist/swapy.min.js"></script>ขั้นตอนที่ 2: Import Dependencies ในไฟล์ที่เราต้องการใช้งาน
// สำหรับใครที่ใช้ Next.js ต้องเพื่ม "use client"; ด้วย เพราะทำงานบน client
import { useEffect, useRef } from "react";
import { createSwapy } from "swapy";
import type {
Swapy,
BeforeSwapEvent,
SwapStartEvent,
SwapEvent,
SwapEndEvent,
} from "swapy";ขั้นตอนที่ 3: ประกาศตัวแปร useRef สำหรับ container และ ตัวเก็บค่า state
const App: React.FC = () => {
const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance
const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapyขั้นตอนที่ 4: สร้าง swapy instance ใน useEffect
useEffect(() => {
// เช็คก่อนว่า container โหลดแล้ว
if (container.current) {
swapy.current = createSwapy(container.current);
// event ของ swapy ก่อนจะลาก จำเป็นต้อง return boolean false คือลากไม่ได้ true คือลากได้
swapy.current.onBeforeSwap((event: BeforeSwapEvent): boolean => {
console.log("swap onBeforeSwap", event);
return true;
});
// event ของ swapy เมื่อเริ่มลาก
swapy.current.onSwapStart((event: SwapStartEvent): void => {
console.log("swap onSwapStart", event);
});
// event ของ swapy ขณะลาก
swapy.current.onSwap((event: SwapEvent): void => {
console.log("swap onSwap", event);
});
// event ของ swapy เมื่อลากเสร็จ
swapy.current.onSwapEnd((event: SwapEndEvent): void => {
console.log("swap onSwapEnd", event);
});
}
return () => {
// เคลียร์ swapy
swapy.current?.destroy();
};
}, []);ขั้นตอนที่ 5: return div container, slot และ item ด้วย slot คือ data-swapy-slot และ item คือ data-swapy-item
return (
<div ref={container}>
{/* slot a */}
<div data-swapy-slot="a">
{/* item a */}
<div data-swapy-item="a">
<div>A</div>
</div>
</div>
{/* slot b */}
<div data-swapy-slot="b">
{/* item a */}
<div data-swapy-item="b">
<div>B</div>
</div>
</div>
</div>
);code เต็ม
import { useEffect, useRef } from "react";
import { createSwapy } from "swapy";
import type {
Swapy,
BeforeSwapEvent,
SwapStartEvent,
SwapEvent,
SwapEndEvent,
} from "swapy";
const App: React.FC = () => {
const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance
const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy
useEffect(() => {
// เช็คก่อนว่า container โหลดแล้ว
if (container.current) {
swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs
// event ของ swapy ก่อนจะลาก จำเป็นต้อง return boolean false คือลากไม่ได้ true คือลากได้
swapy.current.onBeforeSwap((event: BeforeSwapEvent): boolean => {
console.log("swap onBeforeSwap", event);
return true;
});
// event ของ swapy เมื่อเริ่มลาก
swapy.current.onSwapStart((event: SwapStartEvent): void => {
console.log("swap onSwapStart", event);
});
// event ของ swapy ขณะลาก
swapy.current.onSwap((event: SwapEvent): void => {
console.log("swap onSwap", event);
});
// event ของ swapy เมื่อลากเสร็จ
swapy.current.onSwapEnd((event: SwapEndEvent): void => {
console.log("swap onSwapEnd", event);
});
}
return () => {
// เคลียร์ swapy
swapy.current?.destroy();
};
}, []);
return (
<div ref={container}>
{/* slot a */}
<div data-swapy-slot="a">
{/* item a */}
<div data-swapy-item="a">
<div>A</div>
</div>
</div>
{/* slot b */}
<div data-swapy-slot="b">
{/* item a */}
<div data-swapy-item="b">
<div>B</div>
</div>
</div>
</div>
);
};
export default App;และยังมีอีกวิธีสำหรับคนที่ต้องการความ dynamic ให้เนื้อหาภายในมีการ update ตลอดเวลา
ขั้นตอนที่ 1: Install Swapy เข้า Project ของเรา
npm install swapy
หรือ
<script src="https://unpkg.com/swapy/dist/swapy.min.js"></script>ขั้นตอนที่ 2: Import Dependencies ในไฟล์ที่เราต้องการใช้งาน
// สำหรับใครที่ใช้ Next.js ต้องเพื่ม "use client"; ด้วย เพราะทำงานบน client
import { useEffect, useRef, useState, useMemo } from "react";
import { createSwapy, utils } from "swapy";
import type { Swapy, SlotItemMapArray, SwapEvent } from "swapy";
type Item = {
id: string; // สำหรับเก็บ id ของ item และเป็น key ใน slotItemMap
title: string; // สำหรับเก็บชื่อ item
};
const initialItems: Item[] = [
{ id: "1", title: "1" },
{ id: "2", title: "2" },
{ id: "3", title: "3" },
];
let id = 4;ขั้นตอนที่ 3: ประกาศตัวแปร useRef สำหรับ container และ ตัวเก็บค่า state
const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance
const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy
const [items, setItems] = useState<Item[]>(initialItems); // สำหรับเก็บ items ที่จะใช้ใน swapy
const [slotItemMap, setSlotItemMap] = useState<SlotItemMapArray>(
utils.initSlotItemMap(items, "id"), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
);
const slottedItems = useMemo(
() => utils.toSlottedItems(items, "id", slotItemMap), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
[items, slotItemMap],
);
// ทุกครั้งที่ items เปลี่ยนแปลงจะทำการเรียกใช้งาน dynamicSwapy เพื่อให้ swapy ทำการเรียงลำดับ item ใหม่
useEffect(
() =>
utils.dynamicSwapy(
swapy.current,
items,
"id", // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
slotItemMap,
setSlotItemMap,
),
[items],
);ขั้นตอนที่ 4: สร้าง swapy instance ใน useEffect
useEffect(() => {
// เช็คก่อนว่า container โหลดแล้ว
if (container.current) {
swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs
// event ของ swapy ขณะลาก ให้ทำการเปลี่ยน slotItemMap ตามที่ลาก
swapy.current.onSwap((event: SwapEvent): void => {
setSlotItemMap(event.newSlotItemMap.asArray);
});
return () => {
// เคลียร์ swapy
swapy.current?.destroy();
};
}
}, []);ขั้นตอนที่ 5: return div container, slot และ item ด้วย slot คือ data-swapy-slot และ item คือ data-swapy-item
return (
<div className="container" ref={container}>
<div className="items">
{/* แสดง items ที่สามารถลาก */}
{slottedItems.map(({ slotId, itemId, item }, index) => (
<div className="slot" key={slotId} data-swapy-slot={slotId}>
{item && (
<div className="item" data-swapy-item={itemId} key={itemId}>
<span>{index + 1}</span>
<span>{item.title}</span>
{/* ปุ่มลบ item */}
<span
className="delete"
data-swapy-no-drag
onClick={() => {
setItems(items.filter((i) => i.id !== item.id));
}}
></span>
</div>
)}
</div>
))}
</div>
{/* ปุ่มเพิ่ม item */}
<div
className="item item--add"
onClick={() => {
const newItem: Item = { id: `${id}`, title: `${id}` };
setItems([...items, newItem]);
id++;
}}
>
+
</div>
</div>
);code เต็ม
import { useEffect, useRef, useState, useMemo } from "react";
import { createSwapy, utils } from "swapy";
import type { Swapy, SlotItemMapArray, SwapEvent } from "swapy";
type Item = {
id: string; // สำหรับเก็บ id ของ item และเป็น key ใน slotItemMap
title: string; // สำหรับเก็บชื่อ item
};
const initialItems: Item[] = [
{ id: "1", title: "1" },
{ id: "2", title: "2" },
{ id: "3", title: "3" },
];
let id = 4;
const AppDynamic: React.FC = () => {
const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance
const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy
const [items, setItems] = useState<Item[]>(initialItems); // สำหรับเก็บ items ที่จะใช้ใน swapy
const [slotItemMap, setSlotItemMap] = useState<SlotItemMapArray>(
utils.initSlotItemMap(items, "id"), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
);
const slottedItems = useMemo(
() => utils.toSlottedItems(items, "id", slotItemMap), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
[items, slotItemMap],
);
// ทุกครั้งที่ items เปลี่ยนแปลงจะทำการเรียกใช้งาน dynamicSwapy เพื่อให้ swapy ทำการเรียงลำดับ item ใหม่
useEffect(
() =>
utils.dynamicSwapy(
swapy.current,
items,
"id", // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)
slotItemMap,
setSlotItemMap,
),
[items],
);
useEffect(() => {
// เช็คก่อนว่า container โหลดแล้ว
if (container.current) {
swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs
// event ของ swapy ขณะลาก ให้ทำการเปลี่ยน slotItemMap ตามที่ลาก
swapy.current.onSwap((event: SwapEvent): void => {
setSlotItemMap(event.newSlotItemMap.asArray);
});
return () => {
// เคลียร์ swapy
swapy.current?.destroy();
};
}
}, []);
return (
<div className="container" ref={container}>
<div className="items">
{/* แสดง items ที่สามารถลาก */}
{slottedItems.map(({ slotId, itemId, item }, index) => (
<div className="slot" key={slotId} data-swapy-slot={slotId}>
{item && (
<div className="item" data-swapy-item={itemId} key={itemId}>
<span>{index + 1}</span>
<span>{item.title}</span>
{/* ปุ่มลบ item */}
<span
className="delete"
data-swapy-no-drag
onClick={() => {
setItems(items.filter((i) => i.id !== item.id));
}}
></span>
</div>
)}
</div>
))}
</div>
{/* ปุ่มเพิ่ม item */}
<div
className="item item--add"
onClick={() => {
const newItem: Item = { id: `${id}`, title: `${id}` };
setItems([...items, newItem]);
id++;
}}
>
+
</div>
</div>
);
};
export default AppDynamic;


