วิธีใช้ HTML Drag-And-Drop API ใน React

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ ในบทช่วยสอนนี้ เราจะสร้างองค์ประกอบการลากและวาง React สำหรับการอัปโหลดไฟล์และรูปภาพ ในกระบวนการนี้ เราจะเรียนรู้เกี่ยวกับ HTML แบบลากและวาง API นอกจากนี้เรายังจะได้เรียนรู้วิธีการใช้ useReducer hook สำหรับการจัดการสถานะในส่วนประกอบการทำงาน React

API แบบลากแล้ววางเป็นหนึ่งในคุณสมบัติที่ยอดเยี่ยมที่สุดของ HTML ช่วยให้เราใช้คุณสมบัติการลากและวางในเว็บเบราว์เซอร์

ในบริบทปัจจุบัน เราจะทำการลากไฟล์จากภายนอกเบราว์เซอร์ เมื่อวางไฟล์ เราจะใส่ไว้ในรายการและแสดงชื่อไฟล์เหล่านั้น ด้วยไฟล์ที่อยู่ในมือ เราสามารถดำเนินการอื่นๆ กับไฟล์ได้ เช่น อัปโหลดไปยังเซิร์ฟเวอร์คลาวด์

ในบทช่วยสอนนี้ เราจะเน้นที่วิธีการใช้การดำเนินการลากและวางในแอปพลิเคชัน React หากสิ่งที่คุณต้องการคือการใช้งาน JavaScript แบบธรรมดา อันดับแรก คุณอาจต้องการอ่าน “วิธีการสร้างตัวอัปโหลดไฟล์แบบลากและวางด้วย JavaScript วานิลลา” ซึ่งเป็นบทแนะนำที่ยอดเยี่ยมที่เขียนโดยโจเซฟ ซิมเมอร์แมนเมื่อไม่นานมานี้

dragenter , dragleave , dragover , และ drop กิจกรรม

มีแปดเหตุการณ์แบบลากและวางที่แตกต่างกัน แต่ละคนยิงในระยะที่แตกต่างกันของการดำเนินการลากแล้วปล่อย ในบทช่วยสอนนี้ เราจะเน้นที่สี่รายการที่ถูกไล่ออกเมื่อไอเท็มถูกทิ้งลงในโซนดรอป: dragenter , dragleave , dragover และ drop

  1. เหตุการณ์ dragenter เมื่อรายการที่ลากเข้าสู่เป้าหมายการดร็อปที่ถูกต้อง
  2. เหตุการณ์ dragleave เมื่อรายการที่ลากออกจากเป้าหมายการดรอปที่ถูกต้อง
  3. เหตุการณ์ dragover เริ่มทำงานเมื่อมีการลากรายการที่ลากผ่านเป้าหมายการดร็อปที่ถูกต้อง (มันยิงทุกๆ สองสามร้อยมิลลิวินาที)
  4. เหตุการณ์การ 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>

ตอนนี้ดูส่วนประกอบในเบราว์เซอร์และคุณควรเห็นบางอย่างเช่นภาพด้านล่าง

โซนวาง
div ที่จะแปลงเป็นโซนดรอป (ตัวอย่างขนาดใหญ่)

หากคุณกำลังติดตาม repo สาขาที่เกี่ยวข้องคือ 02-start-dragndrop

การจัดการสถานะด้วย useReducer Hook

ขั้นตอนต่อไปของเราคือการเขียนตรรกะสำหรับตัวจัดการเหตุการณ์แต่ละตัวของเรา ก่อนที่เราจะทำเช่นนั้น เราต้องพิจารณาว่าเราตั้งใจจะติดตามไฟล์ที่ถูกทิ้งอย่างไร นี่คือจุดที่เราเริ่มคิดเกี่ยวกับการจัดการของรัฐ

เราจะติดตามสถานะต่อไปนี้ในระหว่างการดำเนินการลากแล้วปล่อย:

  1. dropDepth
    นี่จะเป็นจำนวนเต็ม เราจะใช้มันเพื่อติดตามว่าเราอยู่ลึกแค่ไหนในโซนดรอป ต่อไปฉันจะอธิบายเรื่องนี้ด้วยภาพประกอบ ( ให้เครดิตกับ Egor Egorov สำหรับการส่องแสงให้ฉัน! )
  2. inDropZone
    นี่จะเป็นบูลีน เราจะใช้สิ่งนี้เพื่อติดตามว่าเราอยู่ในโซนดรอปหรือไม่
  3. 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 และ ondragleave
ภาพประกอบของเหตุการณ์ ondragenter และ ondragleave (ตัวอย่างขนาดใหญ่)

เมื่อลากเข้าไปในโซนดรอป ทุกครั้งที่เราไปถึงขอบเขต เหตุการณ์ 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 ถูกเปิดใช้งาน

เมื่อคุณปล่อยไฟล์ภายในดรอปโซน คุณจะเห็นชื่อไฟล์แสดงอยู่ใต้โซนดรอป:

โซนดรอปแสดงความทึบต่ำระหว่าง Dragover
โซนดร็อปแสดงความทึบต่ำในระหว่างการลาก (แสดงตัวอย่างขนาดใหญ่)
รายการไฟล์ที่ดร็อปโซน
รายชื่อไฟล์ที่ดร็อปโซน (ตัวอย่างขนาดใหญ่)

เวอร์ชันสมบูรณ์ของบทช่วยสอนนี้อยู่ที่สาขา 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