---

img

عن المشروع

هذا المشروع يهدف بالاساس لشرح كيفية جعل عناصر React قابلة للسحب و التحريك (Drag & Drop) عن طريق ما يسمى بـ DOM Event Model ما يعني أن هذا يتم مباشرة عن طريق HTML Drag'n Drop API دون اللجوء الى مكتبات خارجية.


الهدف

يسرني أنك قد سئلت, هناك العديد من عناصر واجهة المستخدم (UI) التي يمكن ان تتحسن عندما نضيف لها خاصية التحريك, خصوصا بالنسبة للاجهزة التي تعمل باللمس. عموما نجد هذه العناصر في البرامج التعليمية على سبيل المثال او أي حلول تعتمد بشكل كبير على تجاوب المستخدم مع الواجهة. بعض الامثلة: Trello Boards, Google Calendar, DrawMuzz


النتيجة النهائية

المشروع الذي سنقوم بصنعه:

img

مثال عن كيف يمكن استخدامه في مشروع حقيقي:

img

رابط للتجريب: https://qalamar.github.io/react-dnd-showcase



شرح عام

img img img img



متطلبات

للقيام بتشغيل وصنع المثال, نحتاج أولا الى تنفيذ بعض الاوامر:

npx create-react-app react-dnd
cd react-dnd
npm start

اذا كنت تستعمل Yarn

yarn create react-app react-dnd
cd react-dnd
yarn start



تعليمات

لنتوجه الى ملف App.js

تخزين المعلومات :

لنقم بانشاء جدول يضم معلومات حول العناصر, هذه المعلومات تتظمن: معرف 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",
},
],
});

لا تنسى اضافة تعريف useState في بداية الملف:

import React, { useState } from "react";

معالجة التحريك (Event handlers) :

حينما نضغط على العنصر عند بداية التحريك (onDragStart), نقوم بتهيئة معلومات العنصر للنقل عن طريق dataTransfer.setData

const onDragStart = (event, item) => {
event.dataTransfer.setData("item", item);
};

نمنع المعالجة الافتراضية لأننا نريد تطبيق معالجة خاصة (مقارنة الخانات) عند نهاية التحريك (onDragOver)

const onDragOver = (event) => {
event.preventDefault();
};

عند وضع العنصر داخل الاطار, نقوم بتحويل المعلومات المعدة سابقا في dataTransfer.getData ونقوم بالتأكد بأن العنصر في الاطار المناسب. بعدها نقوم بتحديث الـ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,
});
};

عند حدوث تحريك, نقوم بتحديث جداول الخانات في حالة اضافة عنصر جديد

note

لا تنسى ان تضيف "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 مما يمكننا من تعديل الواجهة بسهولة

tip

هذه الخطوة اختيارية ويمكنك استعمال 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>
);
};

هذا التنسيق يسمح لنا بالحصول على الواجهة التالية img

الان بامكانك تشغيل المشروع

yarn run start

ماذا بعد؟

اذا كنت تريد أن تطور حلول اكثر تعقيدا او أن تظيف Animations دون أن تصنع كل شيء يديويا, انصحك بالاضافات مفتوحة المصدر التالية:

سبب اختياري لهذه الاضافات هو لكونها مشهورة نسبيا وتحظى بدعم مستمر من المطورين. أود أيضا أن اذكر بعض المكتبات مثل Framer Motion التي تحتوي على خصائص مشابهة

كلمة أخيرة

شكرا على القراءة حتى هذه النقطة, أتمنى ان يكون الشرح مفيدا وحظا سعيدا في مشاريعكم المقبلة.