วิธีใช้ HTML Drag-And-Drop API ใน React
เผยแพร่แล้ว: 2022-03-10API แบบลากแล้ววางเป็นหนึ่งในคุณสมบัติที่ยอดเยี่ยมที่สุดของ HTML ช่วยให้เราใช้คุณสมบัติการลากและวางในเว็บเบราว์เซอร์
ในบริบทปัจจุบัน เราจะทำการลากไฟล์จากภายนอกเบราว์เซอร์ เมื่อวางไฟล์ เราจะใส่ไว้ในรายการและแสดงชื่อไฟล์เหล่านั้น ด้วยไฟล์ที่อยู่ในมือ เราสามารถดำเนินการอื่นๆ กับไฟล์ได้ เช่น อัปโหลดไปยังเซิร์ฟเวอร์คลาวด์
ในบทช่วยสอนนี้ เราจะเน้นที่วิธีการใช้การดำเนินการลากและวางในแอปพลิเคชัน React หากสิ่งที่คุณต้องการคือการใช้งาน JavaScript
แบบธรรมดา อันดับแรก คุณอาจต้องการอ่าน “วิธีการสร้างตัวอัปโหลดไฟล์แบบลากและวางด้วย JavaScript วานิลลา” ซึ่งเป็นบทแนะนำที่ยอดเยี่ยมที่เขียนโดยโจเซฟ ซิมเมอร์แมนเมื่อไม่นานมานี้
dragenter
, dragleave
, dragover
, และ drop
กิจกรรม
มีแปดเหตุการณ์แบบลากและวางที่แตกต่างกัน แต่ละคนยิงในระยะที่แตกต่างกันของการดำเนินการลากแล้วปล่อย ในบทช่วยสอนนี้ เราจะเน้นที่สี่รายการที่ถูกไล่ออกเมื่อไอเท็มถูกทิ้งลงในโซนดรอป: dragenter
, dragleave
, dragover
และ drop
- เหตุการณ์
dragenter
เมื่อรายการที่ลากเข้าสู่เป้าหมายการดร็อปที่ถูกต้อง - เหตุการณ์
dragleave
เมื่อรายการที่ลากออกจากเป้าหมายการดรอปที่ถูกต้อง - เหตุการณ์
dragover
เริ่มทำงานเมื่อมีการลากรายการที่ลากผ่านเป้าหมายการดร็อปที่ถูกต้อง (มันยิงทุกๆ สองสามร้อยมิลลิวินาที) - เหตุการณ์การ
drop
อปจะเริ่มขึ้นเมื่อไอเทมดรอปบนเป้าหมายการดรอปที่ถูกต้อง กล่าวคือ ถูกลากไปและปล่อย
เราสามารถเปลี่ยนองค์ประกอบ HTML ใดๆ ให้เป็นเป้าหมายการวางที่ถูกต้องโดยการกำหนด ondragover
และ ondrop
event handler
คุณสามารถเรียนรู้ทั้งหมดเกี่ยวกับเหตุการณ์ทั้งแปดจากเอกสารทางเว็บของ MDN
เหตุการณ์แบบลากแล้วปล่อยใน React
ในการเริ่มต้น ให้โคลน repo บทช่วยสอนจาก URL นี้:
https://github.com/chidimo/react-dnd.git
ตรวจสอบสาขา 01-start
ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง yarn
ไว้ด้วย คุณสามารถดาวน์โหลดได้จาก yarnpkg.com
แต่ถ้าคุณต้องการ ให้สร้างโปรเจ็กต์ React ใหม่และแทนที่เนื้อหาของ App.js ด้วยโค้ดด้านล่าง:
import React from 'react'; import './App.css'; function App() { return ( <div className="App"> <h1>React drag-and-drop component</h1> </div> ); } export default App;
นอกจากนี้ ให้แทนที่เนื้อหาของ App.css ด้วยรูปแบบ CSS ด้านล่าง:
.App { margin: 2rem; text-align: center; } h1 { color: #07F; } .drag-drop-zone { padding: 2rem; text-align: center; background: #07F; border-radius: 0.5rem; box-shadow: 5px 5px 10px #C0C0C0; } .drag-drop-zone p { color: #FFF; } .drag-drop-zone.inside-drag-area { opacity: 0.7; } .dropped-files li { color: #07F; padding: 3px; text-align: left; font-weight: bold; }
หากคุณโคลน repo ให้ออกคำสั่งต่อไปนี้ (ตามลำดับ) เพื่อเริ่มแอป:
yarn # install dependencies yarn start # start the app
ขั้นตอนต่อไปคือการสร้างส่วนประกอบแบบลากและวาง สร้างไฟล์ DragAndDrop.js ภายในโฟลเดอร์ src/
ป้อนฟังก์ชันต่อไปนี้ภายในไฟล์:
import React from 'react'; const DragAndDrop = props => { const handleDragEnter = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); }; const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = e => { e.preventDefault(); e.stopPropagation(); }; return ( <div className={'drag-drop-zone'} onDrop={e => handleDrop(e)} onDragOver={e => handleDragOver(e)} onDragEnter={e => handleDragEnter(e)} onDragLeave={e => handleDragLeave(e)} > <p>Drag files here to upload</p> </div> ); }; export default DragAndDrop;
ใน return div
เราได้กำหนดแอตทริบิวต์ HTML
event handler ของเรา คุณจะเห็นว่าความแตกต่างเพียงอย่างเดียวจาก HTML
ล้วนคือปลอกหุ้มอูฐ
ตอนนี้ div
เป็นเป้าหมายการวางที่ถูกต้องเนื่องจากเราได้กำหนดแอตทริบิวต์ตัวจัดการเหตุการณ์ onDragOver
และ onDrop
นอกจากนี้เรายังกำหนดฟังก์ชันเพื่อจัดการกับเหตุการณ์เหล่านั้น แต่ละฟังก์ชันตัวจัดการเหล่านี้ได้รับวัตถุเหตุการณ์เป็นอาร์กิวเมนต์
สำหรับแต่ละตัวจัดการเหตุการณ์ เราเรียก preventDefault()
เพื่อหยุดเบราว์เซอร์ไม่ให้ทำงานตามค่าเริ่มต้น พฤติกรรมเริ่มต้นของเบราว์เซอร์คือการเปิดไฟล์ที่หลุด นอกจากนี้เรายังเรียก stopPropagation()
เพื่อให้แน่ใจว่าเหตุการณ์จะไม่ถูกเผยแพร่จากองค์ประกอบย่อยไปยังองค์ประกอบหลัก
นำเข้าองค์ประกอบ DragAndDrop
ลงในองค์ประกอบ App
และแสดงผลด้านล่างส่วนหัว
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop /> </div>
ตอนนี้ดูส่วนประกอบในเบราว์เซอร์และคุณควรเห็นบางอย่างเช่นภาพด้านล่าง
หากคุณกำลังติดตาม repo สาขาที่เกี่ยวข้องคือ 02-start-dragndrop
การจัดการสถานะด้วย useReducer
Hook
ขั้นตอนต่อไปของเราคือการเขียนตรรกะสำหรับตัวจัดการเหตุการณ์แต่ละตัวของเรา ก่อนที่เราจะทำเช่นนั้น เราต้องพิจารณาว่าเราตั้งใจจะติดตามไฟล์ที่ถูกทิ้งอย่างไร นี่คือจุดที่เราเริ่มคิดเกี่ยวกับการจัดการของรัฐ
เราจะติดตามสถานะต่อไปนี้ในระหว่างการดำเนินการลากแล้วปล่อย:
-
dropDepth
นี่จะเป็นจำนวนเต็ม เราจะใช้มันเพื่อติดตามว่าเราอยู่ลึกแค่ไหนในโซนดรอป ต่อไปฉันจะอธิบายเรื่องนี้ด้วยภาพประกอบ ( ให้เครดิตกับ Egor Egorov สำหรับการส่องแสงให้ฉัน! ) -
inDropZone
นี่จะเป็นบูลีน เราจะใช้สิ่งนี้เพื่อติดตามว่าเราอยู่ในโซนดรอปหรือไม่ -
FileList
นี่จะเป็นรายการ เราจะใช้มันเพื่อติดตามไฟล์ที่ถูกทิ้งลงในโซนดรอป
ในการจัดการสถานะ React จะจัดเตรียม useState
และ useReducer
hooks เราจะเลือกใช้ useReducer
hook เนื่องจากเราจะจัดการกับสถานการณ์ที่สถานะขึ้นอยู่กับสถานะก่อนหน้า
ตะขอ useReducer
ยอมรับตัวลดประเภท (state, action) => newState
และส่งคืนสถานะปัจจุบันที่จับคู่กับวิธีการ dispatch
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ useReducer
ในเอกสาร React
ภายในองค์ประกอบ App
(ก่อนคำสั่ง return
) ให้เพิ่มรหัสต่อไปนี้:
... const reducer = (state, action) => { switch (action.type) { case 'SET_DROP_DEPTH': return { ...state, dropDepth: action.dropDepth } case 'SET_IN_DROP_ZONE': return { ...state, inDropZone: action.inDropZone }; case 'ADD_FILE_TO_LIST': return { ...state, fileList: state.fileList.concat(action.files) }; default: return state; } }; const [data, dispatch] = React.useReducer( reducer, { dropDepth: 0, inDropZone: false, fileList: [] } ) ...
ตะขอ useReducer
ยอมรับสองอาร์กิวเมนต์: ตัวลดและสถานะเริ่มต้น ส่งคืนสถานะปัจจุบันและฟังก์ชัน dispatch
เพื่ออัปเดตสถานะ สถานะได้รับการอัปเดตโดยจัดส่งการดำเนินการที่มี type
และส่วนของข้อมูลเสริม การอัพเดตที่ทำกับสถานะของคอมโพเนนต์ขึ้นอยู่กับสิ่งที่ส่งคืนจากคำสั่ง case อันเป็นผลมาจากประเภทการดำเนินการ (โปรดทราบว่าสถานะเริ่มต้นของเราคือ object
)
สำหรับแต่ละตัวแปรสถานะ เราได้กำหนดคำสั่ง case ที่เกี่ยวข้องเพื่ออัพเดต การอัปเดตดำเนินการโดยเรียกใช้ฟังก์ชันการ dispatch
ที่ส่งคืนโดย useReducer
ตอนนี้ส่ง data
และ dispatch
เป็น props
ไปยังองค์ประกอบ DragAndDrop
ที่คุณมีในไฟล์ App.js ของคุณ:
<DragAndDrop data={data} dispatch={dispatch} />
ที่ด้านบนขององค์ประกอบ DragAndDrop
เราสามารถเข้าถึงทั้งสองค่าจาก props
const { data, dispatch } = props;
หากคุณกำลังติดตาม repo สาขาที่เกี่ยวข้องคือ 03-define-reducers
มาจบตรรกะของตัวจัดการเหตุการณ์ของเรากัน โปรดทราบว่าจุดไข่ปลาแสดงถึงสองบรรทัด:
e.preventDefault() e.stopPropagation() const handleDragEnter = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth + 1 }); }; const handleDragLeave = e => { ... dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth - 1 }); if (data.dropDepth > 0) return dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }) };
ในภาพประกอบที่ตามมา เราได้วางซ้อนโซน A และ B A เป็นโซนที่เราสนใจ นี่คือที่ที่เราต้องการฟังเหตุการณ์แบบลากและวาง
เมื่อลากเข้าไปในโซนดรอป ทุกครั้งที่เราไปถึงขอบเขต เหตุการณ์ ondragenter
จะถูกไล่ออก สิ่งนี้เกิดขึ้นที่ขอบเขต A-in
และ B-in
เนื่องจากเรากำลังเข้าสู่โซน เราจึงเพิ่ม dropDepth
ในทำนองเดียวกัน เมื่อลากออกจากโซนดรอป ทุกครั้งที่เราไปถึงขอบเขต เหตุการณ์ ondragleave
จะเริ่มทำงาน สิ่งนี้เกิดขึ้นที่ขอบเขต A-out
และ B-out
เนื่องจากเรากำลังออกจากโซน เราจึงลดค่าของ dropDepth
สังเกตว่าเราไม่ได้ตั้งค่า inDropZone
false
ที่ขอบเขต B-out
นั่นคือเหตุผลที่เรามีบรรทัดนี้เพื่อตรวจสอบ dropDepth และส่งคืนจากฟังก์ชัน dropDepth
ที่มากกว่า 0
if (data.dropDepth > 0) return
นี่เป็นเพราะแม้ว่าเหตุการณ์ ondragleave
จะถูกไล่ออก แต่เรายังคงอยู่ในโซน A หลังจากที่เรากด A-out
แล้ว และ dropDepth
ตอนนี้เป็น 0
ที่เราตั้งค่า inDropZone
false
ณ จุดนี้ เราออกจากโซนดรอปทั้งหมดแล้ว
const handleDragOver = e => { ... e.dataTransfer.dropEffect = 'copy'; dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true }); };
ทุกครั้งที่เหตุการณ์นี้เริ่มทำงาน เราตั้งค่า inDropZone
true
สิ่งนี้บอกเราว่าเราอยู่ในโซนดรอปโซน นอกจากนี้เรายังตั้งค่า dropEffect
บนวัตถุ dataTransfer
เพื่อ copy
สำหรับ Mac การดำเนินการนี้จะมีผลในการแสดงเครื่องหมายบวกสีเขียวเมื่อคุณลากรายการไปรอบๆ ในโซนดรอปโซน
const handleDrop = e => { ... let files = [...e.dataTransfer.files]; if (files && files.length > 0) { const existingFiles = data.fileList.map(f => f.name) files = files.filter(f => !existingFiles.includes(f.name)) dispatch({ type: 'ADD_FILE_TO_LIST', files }); e.dataTransfer.clearData(); dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 }); dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false }); } };
เราสามารถเข้าถึงไฟล์ที่ลดลงด้วย e.dataTransfer.files
ค่านี้เป็นอ็อบเจ็กต์ที่คล้ายกับอาร์เรย์ ดังนั้นเราจึงใช้ไวยากรณ์การแพร่กระจายอาร์เรย์เพื่อแปลงเป็นอาร์เรย์ JavaScript
ตอนนี้เราต้องตรวจสอบว่ามีไฟล์อย่างน้อยหนึ่งไฟล์หรือไม่ก่อนที่จะพยายามเพิ่มลงในอาร์เรย์ของไฟล์ เรายังตรวจสอบให้แน่ใจว่าไม่ได้รวมไฟล์ที่มีอยู่แล้วใน fileList
ของเรา ออบเจ็กต์ dataTransfer
จะถูกล้างเพื่อเตรียมพร้อมสำหรับการดำเนินการลากแล้วปล่อยครั้งต่อไป นอกจากนี้เรายังรีเซ็ตค่าของ dropDepth
และ inDropZone
อัปเดต className
ของ div
ในคอมโพเนนต์ DragAndDrop
สิ่งนี้จะเปลี่ยน className
ของ div
ตามเงื่อนไขขึ้นอยู่กับค่าของ data.inDropZone
<div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'} ... > <p>Drag files here to upload</p> </div>
แสดงรายการไฟล์ใน App.js โดยการแมปผ่าน data.fileList
<div className="App"> <h1>React drag-and-drop component</h1> <DragAndDrop data={data} dispatch={dispatch} /> <ol className="dropped-files"> {data.fileList.map(f => { return ( <li key={f.name}>{f.name}</li> ) })} </ol> </div>
ตอนนี้ให้ลองลากและวางไฟล์บางไฟล์ลงในโซนดร็อป คุณจะเห็นว่าเมื่อเราเข้าสู่โซนดร็อป พื้นหลังจะทึบน้อยลงเนื่องจากคลาส inside-drag-area
ถูกเปิดใช้งาน
เมื่อคุณปล่อยไฟล์ภายในดรอปโซน คุณจะเห็นชื่อไฟล์แสดงอยู่ใต้โซนดรอป:
เวอร์ชันสมบูรณ์ของบทช่วยสอนนี้อยู่ที่สาขา 04-finish-handlers
บทสรุป
เราได้เห็นวิธีจัดการการอัปโหลดไฟล์ใน React โดยใช้ HTML
drag-and-drop API นอกจากนี้เรายังได้เรียนรู้วิธีจัดการสถานะด้วย useReducer
hook เราสามารถขยายฟังก์ชัน handleDrop
ของไฟล์ได้ ตัวอย่างเช่น เราสามารถเพิ่มเช็คอื่นเพื่อจำกัดขนาดไฟล์ได้หากต้องการ ซึ่งอาจมาก่อนหรือหลังการตรวจสอบไฟล์ที่มีอยู่ นอกจากนี้เรายังสามารถทำให้โซนดรอปสามารถคลิกได้โดยไม่กระทบต่อฟังก์ชันการลากแล้วปล่อย
ทรัพยากร
- “การอ้างอิง Hooks API:
useReducer
,” React Docs - “HTML Drag-and-Drop API,” เอกสารเว็บ MDN
- “ตัวอย่างการพัฒนาเว็บและ XML โดยใช้ DOM” MDN web docs
- “วิธีสร้างตัวอัปโหลดไฟล์แบบลากและวางด้วย JavaScript วานิลลา” โจเซฟ ซิมเมอร์แมน จาก Smashing Magazine
- “การอัปโหลดไฟล์แบบลากแล้ววางอย่างง่ายในการตอบสนอง” Egor Egorov, Medium