هذا المشروع يهدف بالاساس لشرح كيفية جعل عناصر
React قابلة للسحب و التحريك
(Drag & Drop) عن طريق ما يسمى بـ
DOM Event Model ما يعني أن هذا يتم مباشرة عن طريق
HTML Drag'n Drop API دون اللجوء الى مكتبات خارجية.
يسرني أنك قد سئلت, هناك العديد من عناصر واجهة المستخدم (UI) التي يمكن ان تتحسن عندما نضيف لها خاصية التحريك, خصوصا بالنسبة للاجهزة التي تعمل باللمس. عموما نجد هذه العناصر في البرامج التعليمية على سبيل المثال او أي حلول تعتمد بشكل كبير على تجاوب المستخدم مع الواجهة. بعض الامثلة:
Trello Boards ,
Google Calendar Scheduler ,
DrawMuzz
المشروع الذي سنقوم بصنعه:
مثال عن كيف يمكن استخدامه في مشروع حقيقي:
للقيام بتشغيل وصنع المثال, نحتاج أولا الى تنفيذ بعض الاوامر:
npx create - react - app react - dnd
cd react - dnd
npm start
yarn create react - app react - dnd
cd react - dnd
yarn start
لنقم بانشاء جدول يضم معلومات حول العناصر, هذه المعلومات تتظمن: معرف id
, محتوى العنصر item
والخانة الخاصة به type
.
const [ state , setState ] = useState ( {
items : [
{
id : "1" ,
item : "العنصر الاول" ,
type : "Slot1" ,
} ,
{
id : "2" ,
item : "العنصر الثاني" ,
type : "Slot1" ,
} ,
{
id : "3" ,
item : "العنصر الثالث" ,
type : "Slot2" ,
} ,
{
id : "4" ,
item : "العنصر الرابع" ,
type : "Slot2" ,
} ,
] ,
} ) ;
معالجة التحريك (Event handlers):
حينما نضغط على العنصر عند بداية التحريك (onDragStart) , نقوم بتهيئة معلومات العنصر للنقل عن طريق dataTransfer.setData
const onDragStart = ( event , item ) => {
event . dataTransfer . setData ( "item" , item ) ;
} ;
نمنع المعالجة الافتراضية لأننا نريد تطبيق معالجة خاصة (مقارنة الخانات) عند نهاية التحريك (onDragOver)
const onDragOver = ( event ) => {
event . preventDefault ( ) ;
} ;
عند وضع العنصر داخل الاطار, نقوم بتحويل المعلومات المعدة سابقا في dataTransfer.getData
ونقوم بالتأكد بأن العنصر في الاطار المناسب. بعدها نقوم بتحديث الـstate بالحالة الجديدة
ونقوم بالتأكد بأن العنصر في الاطار المناسب. بعدها نقوم بتحديث الـstate بالحالة الجديدة
const onDrop = ( event , slot ) => {
let item = event . dataTransfer . getData ( "item" ) ;
let items = state . items . filter ( ( task ) => {
if ( task . item === item ) {
task . type = slot ;
}
return task ;
} ) ;
setState ( {
...state ,
items,
} ) ;
} ;
عند حدوث تحريك, نقوم بتحديث جداول الخانات في حالة اضافة عنصر جديد
لا تنسى ان تضيف "dir="rtl الى عناصرك اذا كنت تستخدم اللغة العربية
state . items . forEach ( ( task ) => {
items [ task . type ] . push (
< div
key = { task . id }
onDragStart = { ( event ) => onDragStart ( event , task . item ) }
draggable
dir = "rtl"
>
{ task . item }
< / div >
) ;
} ) ;
في واجهة المستخدم, نقوم بصنع خانتين. كل منهما تعرض العناصر المطابقة لنوعها من جدول العناصر
< div >
< h1 dir = "rtl" > يمكنك سحب العناصر من خانة الى اخرى (Drag & Drop)< / h1 >
< div >
< div
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => onDrop ( event , "Slot1" ) }
>
< h2 > الخانة 1< / h2 >
{ items . Slot1 }
< / div >
< div
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => onDrop ( event , "Slot2" ) }
>
< h2 > الخانة 2< / h2 >
{ items . Slot2 }
< / div >
< / div >
< / div >
حينما تنتهي من اتباع ما سبق, يصبح لديك الملف التالي:
const App = ( ) => {
const [ state , setState ] = useState ( {
items : [
{
id : "1" ,
item : "العنصر الاول" ,
type : "Slot1" ,
} ,
{
id : "2" ,
item : "العنصر الثاني" ,
type : "Slot1" ,
} ,
{
id : "3" ,
item : "العنصر الثالث" ,
type : "Slot2" ,
} ,
{
id : "4" ,
item : "العنصر الرابع" ,
type : "Slot2" ,
} ,
] ,
} ) ;
const onDragStart = ( event , item ) => {
event . dataTransfer . setData ( "item" , item ) ;
} ;
const onDragOver = ( event ) => {
event . preventDefault ( ) ;
} ;
const onDrop = ( event , slot ) => {
let item = event . dataTransfer . getData ( "item" ) ;
let items = state . items . filter ( ( task ) => {
if ( task . item === item ) {
task . type = slot ;
}
return task ;
} ) ;
setState ( {
...state ,
items,
} ) ;
} ;
let items = {
Slot1 : [ ] ,
Slot2 : [ ] ,
} ;
state . items . forEach ( ( task ) => {
items [ task . type ] . push (
< div
key = { task . id }
onDragStart = { ( event ) => onDragStart ( event , task . item ) }
draggable
dir = "rtl"
>
{ task . item }
< / div >
) ;
} ) ;
return (
< div >
< h1 dir = "rtl" > يمكنك سحب العناصر من خانة الى اخرى (Drag & Drop)< / h1 >
< div >
< div
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => onDrop ( event , "Slot1" ) }
>
< h2 > الخانة 1< / h2 >
{ items . Slot1 }
< / div >
< div
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => onDrop ( event , "Slot2" ) }
>
< h2 > الخانة 2< / h2 >
{ items . Slot2 }
< / div >
< / div >
< / div >
) ;
} ;
لجعل المشروع في صيغة قابلة للعرض, سنقوم باضافة TailwindCSS
مما يمكننا من تعديل الواجهة بسهولة
هذه الخطوة اختيارية ويمكنك استعمال CSS فقط أو اي مكتبة تفضل كـBootstrap, Material UI
yarn add tailwindcss - D
npx tailwind init
اصنع ملف src/tailwind.css
وضع بداخله المعلومات التالية:
@tailwind base ;
@tailwind components ;
@tailwind utilities ;
الان توجه الى ملف package.json
وعدل الـscripts كما يلي:
"scripts" : {
"start" : "npm run tailwind:css && react-scripts start" ,
"tailwind:css" : "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css" ,
"build" : "npm run tailwind:css && react-scripts build" ,
...
}
عد الى الملف الرئيسي وطبق التنسيق التالي:
const App = ( ) => {
const [ state , setState ] = useState ( {
items : [
{
id : "1" ,
item : "العنصر الاول" ,
type : "Slot1" ,
} ,
{
id : "2" ,
item : "العنصر الثاني" ,
type : "Slot1" ,
} ,
{
id : "3" ,
item : "العنصر الثالث" ,
type : "Slot2" ,
} ,
{
id : "4" ,
item : "العنصر الرابع" ,
type : "Slot2" ,
} ,
] ,
} ) ;
const onDragStart = ( event , item ) => {
event . dataTransfer . setData ( "item" , item ) ;
} ;
const onDragOver = ( event ) => {
event . preventDefault ( ) ;
} ;
const onDrop = ( event , slot ) => {
let item = event . dataTransfer . getData ( "item" ) ;
let items = state . items . filter ( ( task ) => {
if ( task . item === item ) {
task . type = slot ;
}
return task ;
} ) ;
setState ( {
...state ,
items,
} ) ;
} ;
let items = {
Slot1 : [ ] ,
Slot2 : [ ] ,
} ;
state . items . forEach ( ( task ) => {
items [ task . type ] . push (
< div
key = { task . id }
onDragStart = { ( event ) => onDragStart ( event , task . item ) }
draggable
dir = "rtl"
className = "p-4 m-4 bg-white border-r-8 border-blue-700 shadow-xl"
>
{ task . item }
< / div >
) ;
} ) ;
return (
< div className = "flex flex-wrap pt-32 main" >
< div class = "md:w-3/12 w-1/12 p-4" > < / div >
< div class = "md:w-6/12 w-10/12 p-4" >
< h1 className = "pb-16 text-xl font-bold text-center" dir = "rtl" >
يمكنك سحب العناصر من خانة الى اخرى (Drag & Drop)
< / h1 >
< div className = "flex flex-wrap" >
< div
className = "w-1/2 p-6 shadow-md"
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => {
onDrop ( event , "Slot1" ) ;
} }
>
< h2 className = "font-bold text-center" > الخانة 1< / h2 >
{ items . Slot1 }
< / div >
< div
className = "w-1/2 p-6 shadow-md"
onDragOver = { ( event ) => onDragOver ( event ) }
onDrop = { ( event ) => onDrop ( event , "Slot2" ) }
>
< h2 className = "font-bold text-center" > الخانة 2< / h2 >
{ items . Slot2 }
< / div >
< / div >
< / div >
< div class = "md:w-3/12 w-1/12 p-4" > < / div >
< / div >
) ;
} ;
هذا التنسيق يسمح لنا بالحصول على الواجهة التالية
الان بامكانك تشغيل المشروع
اذا كنت تريد أن تطور حلول اكثر تعقيدا او أن تظيف Animations دون أن تصنع كل شيء يديويا, انصحك بالاضافات مفتوحة المصدر التالية:
سبب اختياري لهذه الاضافات هو لكونها مشهورة نسبيا وتحظى بدعم مستمر من المطورين. أود أيضا أن اذكر بعض المكتبات مثل
Framer Motion التي تحتوي على خصائص مشابهة
شكرا على القراءة حتى هذه النقطة, أتمنى ان يكون الشرح مفيدا وحظا سعيدا في مشاريعكم المقبلة.