افزودن قابلیت کشیدن و رها کردن به اپلیکیشن React — از صفر تا صد

۲۳۲ بازدید
آخرین به‌روزرسانی: ۲۰ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
افزودن قابلیت کشیدن و رها کردن به اپلیکیشن React — از صفر تا صد

قابلیت کشیدن و رها کردن (Drag and Drop) یکی از پراستفاده‌ترین قابلیت‌ها در وب‌اپلیکیشن‌ها محسوب می‌شود. این قابلیت یک روش شهودی برای دستکاری داده‌ها ارائه می‌کند. React یک کتابخانه عالی است که می‌توان با استفاده از آن UI وب را ساخت و از این رو کتابخانه‌های با کیفیت بالایی به شکل کامپوننت‌های ری‌اکت برای افزودن قابلیت Drag and Drop به اپلیکیشن React نوشته شده‌اند.

997696

کتابخانه react-beautiful-dnd از سوی Atlassian، سازنده سیستم مدیریت وظیفه JIRA طراحی شده است. این کتابخانه Drag and Drop یکی از ساده‌ترین کتابخانه‌ها برای گنجاندن در اپلیکیشن محسوب می‌شود.

در این مقاله یک لیست to-do می‌سازیم که از قابلیت Drag and Drop بهره می‌گیرد. در این مثال یک صفحه اصلی با یک فرم داریم که یک وظیفه در ابتدای صفحه ایجاد می‌شود، سپس در ادامه دو لیست به صورت کنار هم قرار دارند که لیست سمت چپ آیتم to-do را نمایش می‌دهد و لیست سمت راست نیز آیتم‌های انجام یافته را نمی‌دهد. همچنین یک روش برای حذف وظیفه از هر یک از 2 لیست در اختیار کاربر قرار می‌گیرد. یک Redux store برای ذخیره‌سازی کل لیست کارها مور استفاده قرار می‌گیرد.

برای شروع به ساخت اپلیکیشن، از یکی از ساده‌ترین روش‌های ممکن یعنی برنامه Create React App که فیسبوک ارائه کرده است کمک می‌گیریم. برای استفاده از آن باید دستور زیر را اجرا کنیم:

npx create-react-app todo-app

دستور فوق یک پوشه پروژه ایجاد می‌کند که کدهای اولیه اپلیکیشن نیز درون آن قرار دارند.

سپس برخی کتابخانه‌های مورد نیاز را نصب می‌کنیم. ما به یک کلاینت HTTP نیاز داریم، همچنین کتابخانه‌های react-beautiful-dnd، Bootstrap، Redux و React Router را نصب می‌کنیم. ما یک کلاینت HTTP برای ایجاد درخواست‌های HTTP می‌سازیم. با استفاده از بوت‌استرپ استایل‌بندی آسانی خواهیم داشت و از ری‌اکت روتر برای مسیریابی سمت کلاینت بهره خواهیم جست. دستور زیر را اجرا کنید تا همه کتابخانه‌های فوق نصب شوند:

npm i axios bootstrap react-bootstrap formik yup react-beautiful-dnd react-router-dom react-redux

از Axios به عنوان کلاینت HTTP خود استفاده می‌کنیم و Formik و Yup کتابخانه‌های اعتبارسنجی فرم هستند که می‌توان به همراه React Bootstrap برای ذخیره موارد وارد شدن در فرم استفاده کرد.

زمانی که همه کتابخانه‌های فوق نصب شدند، می‌توانیم شروع به نوشتن اپلیکیشن بکنیم. همه چیز را در پوشه src قرار می‌دهیم، مگر این که طور دیگری ذکر شده باشد. در آغاز یک فایل به نام actionCreator.js می‌سازیم و کد زیر را در آن قرار می‌دهیم:

1import { SET_TASKS } from './actions';
2const setTasks = (tasks) => {
3    return {
4        type: SET_TASKS,
5        payload: tasks
6    }
7};
8export { setTasks };

سپس فایلی به نام action.js ایجاد می‌کنیم و کد زیر را در آن می‌نویسیم:

1const SET_TASKS = 'SET_TASKS';
2export { SET_TASKS };

این دو فایل با همدیگر یک اکشن می‌سازند که به ریداکس استور که در ادامه خواهیم ساخت، ارسال می‌کنیم. سپس در فایل App.js کد موجود را با کد زیر تعویض می‌کنیم:

1import React from 'react';
2import { Router, Route, Link } from "react-router-dom";
3import HomePage from './HomePage';
4import { createBrowserHistory as createHistory } from 'history'
5import Navbar from 'react-bootstrap/Navbar';
6import Nav from 'react-bootstrap/Nav';
7import './App.css';
8const history = createHistory();
9function App() {
10  return (
11    <div className="App">
12      <Router history={history}>
13        <Navbar bg="primary" expand="lg" variant="dark" >
14          <Navbar.Brand href="#home">Drag and Drop App</Navbar.Brand>
15          <Navbar.Toggle aria-controls="basic-navbar-nav" />
16          <Navbar.Collapse id="basic-navbar-nav">
17            <Nav className="mr-auto">
18              <Nav.Link href="/">Home</Nav.Link>
19            </Nav>
20          </Navbar.Collapse>
21        </Navbar>
22        <Route path="/" exact component={HomePage} />
23      </Router>
24    </div>
25  );
26}
27export default App;

کد فوق یک نوار ناوبری در بخش فوقانی اضافه می‌کند و امکان نمایش مسیرهایی که در ادامه تعریف می‌کنیم را فراهم می‌سازد. صفحه اصلی فعلاً تنها مسیر است. در فایل HomePage.js کد زیر را می‌نویسیم:

1import React from 'react';
2import { useState, useEffect } from 'react';
3import './HomePage.css';
4import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
5import { connect } from 'react-redux';
6import TaskForm from './TaskForm';
7import { editTask, getTasks, deleteTask } from './requests';
8function HomePage({ tasks }) {
9  const [items, setItems] = useState([]);
10  const [todoItems, setTodoItems] = useState([]);
11  const [doneItems, setDoneItems] = useState([]);
12  const [initialized, setInitialized] = useState(false);
13const onDragEnd = async (evt) => {
14    const { source } = evt;
15    let item = {};
16    if (source.droppableId == "todoDroppable") {
17      item = todoItems[source.index];
18      item.done = true;
19    }
20    else {
21      item = doneItems[source.index];
22      item.done = false;
23    }
24    await editTask(item);
25    await getTodos();
26  };
27const setAllItems = (data) => {
28    if (!Array.isArray(data)) {
29      return;
30    }
31    setItems(data);
32    setTodoItems(data.filter(i => !i.done));
33    setDoneItems(data.filter(i => i.done));
34  }
35const getTodos = async () => {
36    const response = await getTasks();
37    setAllItems(response.data);
38    setInitialized(true);
39  }
40const removeTodo = async (task) => {
41    await deleteTask(task.id);
42    await getTodos();
43  }
44useEffect(() => {
45    setAllItems(tasks);
46    if (!initialized) {
47      getTodos();
48    }
49  }, [tasks]);
50return (
51    <div className="App">
52      <div className='col-12'>
53        <TaskForm />
54        <br />
55      </div>
56      <div className='col-12'>
57        <div className='row list'>
58          <DragDropContext onDragEnd={onDragEnd}>
59            <Droppable droppableId="todoDroppable">
60              {(provided, snapshot) => (
61                <div
62                  className='droppable'
63                  ref={provided.innerRef}
64                >
65                   
66                  <h2>To Do</h2>
67                  <div class="list-group">
68                    {todoItems.map((item, index) => (
69                      <Draggable
70                        key={item.id}
71                        draggableId={item.id}
72                        index={index}
73                      >
74                        {(provided, snapshot) => (
75                          <div
76                            className='list-group-item '
77                            ref={provided.innerRef}
78                            {...provided.draggableProps}
79                            {...provided.dragHandleProps}
80                          >
81                            {item.description}
82                            <a onClick={removeTodo.bind(this, item)}>
83                              <i class="fa fa-close"></i>
84                            </a>
85                          </div>
86                        )}
87                      </Draggable>
88                    ))}
89                  </div>
90                  {provided.placeholder}
91                </div>
92              )}
93            </Droppable>
94            <Droppable droppableId="doneDroppable">
95              {(provided, snapshot) => (
96                <div
97                  className='droppable'
98                  ref={provided.innerRef}
99                >
100                   
101                  <h2>Done</h2>
102                  <div class="list-group">
103                    {doneItems.map((item, index) => (
104                      <Draggable
105                        key={item.id}
106                        draggableId={item.id}
107                        index={index}
108                      >
109                        {(provided, snapshot) => (
110                          <div
111                            className='list-group-item'
112                            ref={provided.innerRef}
113                            {...provided.draggableProps}
114                            {...provided.dragHandleProps}
115                          >
116                            {item.description}
117                            <a onClick={removeTodo.bind(this, item)}>
118                              <i class="fa fa-close"></i>
119                            </a>
120                          </div>
121                        )}
122                      </Draggable>
123                    ))}
124                  </div>
125                  {provided.placeholder}
126                </div>
127              )}
128            </Droppable>
129          </DragDropContext>
130        </div>
131      </div>
132    </div>
133  );
134}
135const mapStateToProps = state => {
136  return {
137    tasks: state.tasks,
138  }
139}
140export default connect(
141  mapStateToProps,
142  null
143)(HomePage);

این همان جایی است که منطق «کشیدن و رها کردن» قرار می‌گیرد. ما دو کامپوننت Droppable داریم که در آن‌ها از 2 لیست برای ذخیره آیتم‌های to-do و آیتم‌های Done استفاده می‌کنیم. تنها دستگیره کشیدن و رها کردن که نیاز داریم، دستگیره onDragEnd است. این همان جایی است که فلگ done را برای آیتم وظیفه مرتبط با source که آیتم از آن کشیده شده است به‌روزرسانی می‌کنیم. اگر از کامپوننت Droppable آیتمی را با مشخصه droppableId به نام todoDroppable بکشیم، در این صورت مقدار done را روی true تنظیم می‌کنیم. در غیر این صورت مقدار Done را روی false تعیین می‌کنیم. پس از انجام این کار، وظیفه را به‌روزرسانی می‌کنیم و سپس آخرین وظایف را دریافت کرده و آن‌ها را نمایش می‌دهیم. همچنین تابعی به نام removeTodo برای حذف یک وظیفه داریم. زمانی که وظیفه‌ای حذف می‌شود، آخرین لیست از بک‌اند دریافت می‌شود. تابع setAllItems برای توزیع وظایف در لیست‌های آیتم‌های to-do و done استفاده می‌شود.

در تابع useEffect یک آرگومان دوم با آرایه‌ای از tasks به عنوان آرگومان داریم. این آرایه برای هدفگیری یک callback به نام useEffect که در زمان تغییر یافتن tasks فراخوانی می‌شود، مورد استفاده قرار می‌گیرد. همین callback در صورتی که tasks با هر prop دیگر عوض شود، می‌تواند برای مدیریت هر تغییر prop دیگر نیزاستفاده شود.

در فایل HomePage.css کد زیر را می‌نویسیم:

1.item {
2  padding: 20px;
3  border: 1px solid black;
4  width: 40vw;
5}
6.list {
7  margin-left: 0px;
8}
9.list-group-item {
10  display: flex;
11  justify-content: space-between;
12}
13.list-group-item a {
14  cursor: pointer;
15}
16.droppable {
17  min-height: 100px;
18  width: 40vw;
19  margin-right: 9vw;
20}

این کد برای استایل‌بندی لیست‌ها با افزودن مقداری فضابندی استفاده می‌شود. برای افزودن وظایف، یک کامپوننت اختصاصی به نام TaskForm برای اضافه کردن وظیفه داریم. فایلی به نام TaskForm.js ایجاد کرده و کد زیر را در آن قرار می‌دهیم:

1import React from 'react';
2import { Formik } from 'formik';
3import Form from 'react-bootstrap/Form';
4import Col from 'react-bootstrap/Col';
5import Button from 'react-bootstrap/Button';
6import * as yup from 'yup';
7import { addTask, getTasks } from './requests';
8import { connect } from 'react-redux';
9import { setTasks } from './actionCreators';
10import './TaskForm.css';
11const schema = yup.object({
12    description: yup.string().required('Description is required'),
13});
14function ContactForm({ setTasks }) {
15    const handleSubmit = async (evt) => {
16        const isValid = await schema.validate(evt);
17        if (!isValid) {
18            return;
19        }
20        await addTask(evt);
21        const response = await getTasks();
22        setTasks(response.data);
23    }
24return (
25        <div className="form">
26            <Formik
27                validationSchema={schema}
28                onSubmit={handleSubmit}
29            >
30                {({
31                    handleSubmit,
32                    handleChange,
33                    handleBlur,
34                    values,
35                    touched,
36                    isInvalid,
37                    errors,
38                }) => (
39                        <Form noValidate onSubmit={handleSubmit}>
40                            <Form.Row>
41                                <Form.Group as={Col} md="12" controlId="firstName">
42                                    <Form.Label>
43                                        <h4>Add Task</h4>
44                                    </Form.Label>
45                                    <Form.Control
46                                        type="text"
47                                        name="description"
48                                        placeholder="Task Description"
49                                        value={values.description || ''}
50                                        onChange={handleChange}
51                                        isInvalid={touched.description && errors.description}
52                                    />
53                                    <Form.Control.Feedback type="invalid">
54                                        {errors.description}
55                                    </Form.Control.Feedback>
56                                </Form.Group>
57                            </Form.Row>
58                            <Button type="submit" style={{ 'marginRight': '10px' }}>Save</Button>
59                        </Form>
60                    )}
61            </Formik>
62        </div>
63    );
64}
65ContactForm.propTypes = {
66}
67const mapStateToProps = state => {
68    return {
69        tasks: state.tasks,
70    }
71}
72const mapDispatchToProps = dispatch => ({
73    setTasks: tasks => dispatch(setTasks(tasks))
74})
75export default connect(
76    mapStateToProps,
77    mapDispatchToProps
78)(ContactForm);

اینک فرم افزودن وظیفه را به همراه اعتبارسنجی فرم داریم که بررسی می‌کند آیا توضیحات پر شده‌اند یا نه. زمانی که وظیفه مجاز تلقی شد، آن را به بک‌اند تحویل می‌دهیم، سپس آخرین وظایف را دریافت کرده و در ریداکس استور ذخیره می‌کنیم. این همه کارهایی است که در تابع handleSubmit اجرا می‌شود. توجه داشته باشید که ما اینک شیء schema را با کتابخانه Yup ایجاد کردیم و سپس آن را به شیء validationSchema در کامپوننت Formik ارسال کردیم، در ادامه کامپوننت Formik تابع handleChange، شیء values و شیء errors را ارائه می‌کند که مستقیماً در فرم بوت‌استرپ ری‌اکت استفاده می‌کنیم.

بدین ترتیب از نوشتن دستی همه کدهای دستگیره‌های تغییر معاف می‌شویم. ضمناً باید || ‘’ را نیز در انتهای prop به نام values داشته باشیم تا prop به نام value همواره در حالت تعریف شده باقی بماند و از تحریک خطاهای ورودی ناخواسته جلوگیری شود. mapStateToProps در انتهای فایل، اقدام به نگاشت حالت tasks در store به props به نام tasks در کامپوننت می‌کند و mapDispatchToProps نیز تابع دیسپچ setTasks را نگاشت می‌کند که از آن برای به‌روزرسانی store با وظایف بر اساس props کامپوننت TaskForm استفاده می‌کنیم.

سپس در فایل index.js کد موجود را با کد زیر عوض می‌کنیم:

1import React from 'react';
2import ReactDOM from 'react-dom';
3import './index.css';
4import App from './App';
5import * as serviceWorker from './serviceWorker';
6import { tasksReducer } from './reducers';
7import { Provider } from 'react-redux'
8import { createStore, combineReducers } from 'redux'
9const taskskApp = combineReducers({
10    tasks: tasksReducer,
11})
12const store = createStore(taskskApp);
13ReactDOM.render(
14    <Provider store={store}>
15        <App />
16    </Provider>
17    , document.getElementById('root'));
18// If you want your app to work offline and load faster, you can change
19// unregister() to register() below. Note this comes with some pitfalls.
20// Learn more about service workers: https://bit.ly/CRA-PWA
21serviceWorker.unregister();

بدین ترتیب اینک می‌توانیم از ریداکس استور که در اپلیکیشن خود ایجاد کردیم، استفاده کنیم. سپس فایلی به نام reducers.js ایجاد کرده و کد زیر را اضافه می‌کنیم:

1import { SET_TASKS } from './actions';
2function tasksReducer(state = {}, action) {
3    switch (action.type) {
4        case SET_TASKS:
5            state = JSON.parse(JSON.stringify(action.payload));
6            return state;
7        default:
8            return state
9    }
10}
11export { tasksReducer };

بنابراین می‌توانیم وظایف خود را در این store ذخیره کنیم. سپس فایلی به نام requests.js می‌سازیم که برای ذخیره‌سازی کد تابع‌ها جهت ایجاد درخواست‌های HTTP که ایجاد کرده‌ایم استفاده می‌شود. کد زیر را در فایل بنویسید:

1const APIURL = 'http://localhost:3000';
2const axios = require('axios');
3export const getTasks = () => axios.get(`${APIURL}/tasks`);
4export const addTask = (data) => axios.post(`${APIURL}/tasks`, data);
5export const editTask = (data) => axios.put(`${APIURL}/tasks/${data.id}`, data);
6export const deleteTask = (id) => axios.delete(`${APIURL}/tasks/${id}`);

کدهای فوق امکان ایجاد عملیات CRUD را روی وظایف فراهم می‌سازند. این عملیات در HomePage و کامپوننت و TaskForm استفاده می‌شود. در ادامه کدهای موجود فایل public/index.html را با کد زیر عوض می‌کنیم:

1<!DOCTYPE html>
2
3<html lang="en">
4
5<head>
6
7<meta charset="utf-8" />
8
9<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
10
11<meta name="viewport" content="width=device-width, initial-scale=1" />
12
13<meta name="theme-color" content="#000000" />
14
15<meta name="description" content="Web site created using create-react-app" />
16
17<link rel="apple-touch-icon" href="logo192.png" />
18
19<!--
20
21manifest.json provides metadata used when your web app is installed on a
22
23user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
24
25-->
26
27<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
28
29<!--
30
31Notice the use of%PUBLIC_URL% in the tags above.
32
33It will be replaced with the URL of the `public` folder during the build.
34
35Only files inside the `public` folder can be referenced from the HTML.
36
37Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
38
39work correctly both with client-side routing and a non-root public URL.
40
41Learn how to configure a non-root public URL by running `npm run build`.
42
43-->
44
45<title>React Drag and Drop App</title>
46
47<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
48
49integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
50
51<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"
52
53integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
54
55</head>
56
57<body>
58
59<noscript>You need to enable JavaScript to run this app.</noscript>
60
61<div id="root"></div>
62
63<!--
64
65This HTML file is a template.
66
67If you open it directly in the browser, you will see an empty page.
68
69You can add webfonts, meta tags, or analytics to this file.
70
71The build step will place the bundled scripts into the <body> tag.
72
73To begin the development, run `npm start` or `yarn start`.
74
75To create a production bundle, use `npm run build` or `yarn build`.
76
77-->
78
79</body>
80
81</html>

ما در کد فوق، بوت‌استرپ و CSS و آیکون Font Awesome را اضافه کردیم و از این رو می‌توانیم از استایل بوت‌استرپ استفاده کنیم و آیکون Close را که در تگ i مربوط به HomePage استفاده می‌شود، به دست می‌آوریم.

برای آغاز به کار بک‌اند، ابتدا پکیج json-server را با اجرای دستور زیر نصب می‌کنیم:

npm i json-server

سپس به پوشه پروژه می‌رویم و دستور زیر را اجرا می‌کنیم:

json-server --watch db.json

در فایل db.json، متن را به صورت زیر تغییر می‌دهیم:

1{
2  "tasks": [
3  ]
4}

بدین ترتیب یک نقطه انتهایی به نام tasks داریم که در requests.js تعریف شده است.

قابلیت Drag and Drop

قابلیت Drag and Drop

بدین ترتیب به پایان مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
javascript-in-plain-english
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *