วิธีการทำ Drag to Swap หรือ การลากและวางใน React และ Next.js ด้วย Swapy
06 Mar 2025 04:007 months ago

เคยรู้สึกไหมว่าการจัดเรียงข้อมูลหรือองค์ประกอบบนเว็บไซต์และแอปพลิเคชันเป็นเรื่องยุ่งยาก? วันนี้เราจะมาพาคุณสำรวจเทคนิคสุดล้ำที่จะเปลี่ยนวิธีคิดในการออกแบบอินเตอร์เฟซของคุณ - นั่นก็คือ "Drag to Swap"
เทคนิคนี้ไม่เพียงแต่ทำให้การใช้งานง่ายขึ้น แต่ยังเพิ่มประสบการณ์ผู้ใช้งาน ช่วยเพิ่ม interaction มากขึ้น ด้วยวันนี้เราจะมาแนะนำ dependency ตัวหนึ่งนั้นก็คือ Swapy สามารถใช้ได้ทั้ง
React
Vue
Svelte
โดยในวันนี้จะมาแสดงวิธีการง่ายๆ สำหรับการทำ Drag to Swap บน React (Next.js) กันครับ
อ่านเพิ่มเติมได้ที่
https://swapy.tahazsh.com/docs/installation/และยังมีอีกวิธีสำหรับคนที่ต้องการความ dynamic ให้เนื้อหาภายในมีการ update ตลอดเวลา
1npm install swapy2หรือ3<script src="https://unpkg.com/swapy/dist/swapy.min.js"></script>
1// สำหรับใครที่ใช้ Next.js ต้องเพื่ม "use client"; ด้วย เพราะทำงานบน client2import { useEffect, useRef } from "react";3import { createSwapy } from "swapy";4import type {5Swapy,6BeforeSwapEvent,7SwapStartEvent,8SwapEvent,9SwapEndEvent,10} from "swapy";
1const App: React.FC = () => {2const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance3const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy
1useEffect(() => {2// เช็คก่อนว่า container โหลดแล้ว3if (container.current) {4swapy.current = createSwapy(container.current);56// event ของ swapy ก่อนจะลาก จำเป็นต้อง return boolean false คือลากไม่ได้ true คือลากได้7swapy.current.onBeforeSwap((event: BeforeSwapEvent): boolean => {8console.log("swap onBeforeSwap", event);9return true;10});11// event ของ swapy เมื่อเริ่มลาก12swapy.current.onSwapStart((event: SwapStartEvent): void => {13console.log("swap onSwapStart", event);14});15// event ของ swapy ขณะลาก16swapy.current.onSwap((event: SwapEvent): void => {17console.log("swap onSwap", event);18});19// event ของ swapy เมื่อลากเสร็จ20swapy.current.onSwapEnd((event: SwapEndEvent): void => {21console.log("swap onSwapEnd", event);22});23}2425return () => {26// เคลียร์ swapy27swapy.current?.destroy();28};29}, []);
1return (2<div ref={container}>34{/* slot a */}5<div data-swapy-slot="a">6{/* item a */}7<div data-swapy-item="a">8<div>A</div>9</div>10</div>1112{/* slot b */}13<div data-swapy-slot="b">14{/* item a */}15<div data-swapy-item="b">16<div>B</div>17</div>18</div>19</div>20);
1import { useEffect, useRef } from "react";2import { createSwapy } from "swapy";3import type {4Swapy,5BeforeSwapEvent,6SwapStartEvent,7SwapEvent,8SwapEndEvent,9} from "swapy";1011const App: React.FC = () => {12const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance13const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy1415useEffect(() => {16// เช็คก่อนว่า container โหลดแล้ว17if (container.current) {18swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs1920// event ของ swapy ก่อนจะลาก จำเป็นต้อง return boolean false คือลากไม่ได้ true คือลากได้21swapy.current.onBeforeSwap((event: BeforeSwapEvent): boolean => {22console.log("swap onBeforeSwap", event);23return true;24});25// event ของ swapy เมื่อเริ่มลาก26swapy.current.onSwapStart((event: SwapStartEvent): void => {27console.log("swap onSwapStart", event);28});29// event ของ swapy ขณะลาก30swapy.current.onSwap((event: SwapEvent): void => {31console.log("swap onSwap", event);32});33// event ของ swapy เมื่อลากเสร็จ34swapy.current.onSwapEnd((event: SwapEndEvent): void => {35console.log("swap onSwapEnd", event);36});37}3839return () => {40// เคลียร์ swapy41swapy.current?.destroy();42};43}, []);4445return (46<div ref={container}>4748{/* slot a */}49<div data-swapy-slot="a">50{/* item a */}51<div data-swapy-item="a">52<div>A</div>53</div>54</div>5556{/* slot b */}57<div data-swapy-slot="b">58{/* item a */}59<div data-swapy-item="b">60<div>B</div>61</div>62</div>63</div>64);65};6667export default App;
1npm install swapy2หรือ3<script src="https://unpkg.com/swapy/dist/swapy.min.js"></script>
1// สำหรับใครที่ใช้ Next.js ต้องเพื่ม "use client"; ด้วย เพราะทำงานบน client2import { useEffect, useRef, useState, useMemo } from "react";3import { createSwapy, utils } from "swapy";4import type { Swapy, SlotItemMapArray, SwapEvent } from "swapy";56type Item = {7id: string; // สำหรับเก็บ id ของ item และเป็น key ใน slotItemMap8title: string; // สำหรับเก็บชื่อ item9};1011const initialItems: Item[] = [12{ id: "1", title: "1" },13{ id: "2", title: "2" },14{ id: "3", title: "3" },15];1617let id = 4;
1const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance2const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy3const [items, setItems] = useState<Item[]>(initialItems); // สำหรับเก็บ items ที่จะใช้ใน swapy4const [slotItemMap, setSlotItemMap] = useState<SlotItemMapArray>(5utils.initSlotItemMap(items, "id"), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)6);7const slottedItems = useMemo(8() => utils.toSlottedItems(items, "id", slotItemMap), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)9[items, slotItemMap],10);1112// ทุกครั้งที่ items เปลี่ยนแปลงจะทำการเรียกใช้งาน dynamicSwapy เพื่อให้ swapy ทำการเรียงลำดับ item ใหม่13useEffect(14() =>15utils.dynamicSwapy(16swapy.current,17items,18"id", // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)19slotItemMap,20setSlotItemMap,21),22[items],23);
1useEffect(() => {2// เช็คก่อนว่า container โหลดแล้ว3if (container.current) {4swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs56// event ของ swapy ขณะลาก ให้ทำการเปลี่ยน slotItemMap ตามที่ลาก7swapy.current.onSwap((event: SwapEvent): void => {8setSlotItemMap(event.newSlotItemMap.asArray);9});1011return () => {12// เคลียร์ swapy13swapy.current?.destroy();14};15}16}, []);
1return (2<div className="container" ref={container}>3<div className="items">4{/* แสดง items ที่สามารถลาก */}5{slottedItems.map(({ slotId, itemId, item }, index) => (6<div className="slot" key={slotId} data-swapy-slot={slotId}>7{item && (8<div className="item" data-swapy-item={itemId} key={itemId}>9<span>{index + 1}</span>10<span>{item.title}</span>11{/* ปุ่มลบ item */}12<span13className="delete"14data-swapy-no-drag15onClick={() => {16setItems(items.filter((i) => i.id !== item.id));17}}18></span>19</div>20)}21</div>22))}23</div>2425{/* ปุ่มเพิ่ม item */}26<div27className="item item--add"28onClick={() => {29const newItem: Item = { id: `${id}`, title: `${id}` };30setItems([...items, newItem]);31id++;32}}33>34+35</div>36</div>37);
1import { useEffect, useRef, useState, useMemo } from "react";2import { createSwapy, utils } from "swapy";3import type { Swapy, SlotItemMapArray, SwapEvent } from "swapy";45type Item = {6id: string; // สำหรับเก็บ id ของ item และเป็น key ใน slotItemMap7title: string; // สำหรับเก็บชื่อ item8};910const initialItems: Item[] = [11{ id: "1", title: "1" },12{ id: "2", title: "2" },13{ id: "3", title: "3" },14];1516let id = 4;1718const AppDynamic: React.FC = () => {19const swapy = useRef<Swapy | null>(null); // สำหรับการเก็บ swapy instance20const container = useRef<HTMLDivElement | null>(null); // สำหรับการเก็บ container ของ swapy21const [items, setItems] = useState<Item[]>(initialItems); // สำหรับเก็บ items ที่จะใช้ใน swapy22const [slotItemMap, setSlotItemMap] = useState<SlotItemMapArray>(23utils.initSlotItemMap(items, "id"), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)24);25const slottedItems = useMemo(26() => utils.toSlottedItems(items, "id", slotItemMap), // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)27[items, slotItemMap],28);2930// ทุกครั้งที่ items เปลี่ยนแปลงจะทำการเรียกใช้งาน dynamicSwapy เพื่อให้ swapy ทำการเรียงลำดับ item ใหม่31useEffect(32() =>33utils.dynamicSwapy(34swapy.current,35items,36"id", // "id" ต้องเป็น string เท่านั่นเป็นตัวเลขไม่ได้ (v.1.0.5)37slotItemMap,38setSlotItemMap,39),40[items],41);4243useEffect(() => {44// เช็คก่อนว่า container โหลดแล้ว45if (container.current) {46swapy.current = createSwapy(container.current, {}); // สามารถใส่ options เพิ่มเติมได้เช็คได้ที่ docs4748// event ของ swapy ขณะลาก ให้ทำการเปลี่ยน slotItemMap ตามที่ลาก49swapy.current.onSwap((event: SwapEvent): void => {50setSlotItemMap(event.newSlotItemMap.asArray);51});5253return () => {54// เคลียร์ swapy55swapy.current?.destroy();56};57}58}, []);5960return (61<div className="container" ref={container}>62<div className="items">63{/* แสดง items ที่สามารถลาก */}64{slottedItems.map(({ slotId, itemId, item }, index) => (65<div className="slot" key={slotId} data-swapy-slot={slotId}>66{item && (67<div className="item" data-swapy-item={itemId} key={itemId}>68<span>{index + 1}</span>69<span>{item.title}</span>70{/* ปุ่มลบ item */}71<span72className="delete"73data-swapy-no-drag74onClick={() => {75setItems(items.filter((i) => i.id !== item.id));76}}77></span>78</div>79)}80</div>81))}82</div>8384{/* ปุ่มเพิ่ม item */}85<div86className="item item--add"87onClick={() => {88const newItem: Item = { id: `${id}`, title: `${id}` };89setItems([...items, newItem]);90id++;91}}92>93+94</div>95</div>96);97};9899export default AppDynamic;