Tài Liệu Học Tập
No Result
View All Result
  • Đề Thi
  • Lớp 12
    • Lịch Sử Lớp 12
    • Địa Lí Lớp 12
    • Ngữ Văn Lớp 12
    • GD KTPL Lớp 12
    • Toán Lớp 12
    • Tiếng Anh Lớp 12
    • Hóa Học Lớp 12
    • Sinh Học Lớp 12
    • Vật Lí Lớp 12
  • Lớp 11
    • Toán Lớp 11
    • Ngữ Văn Lớp 11
    • Tiếng Anh Lớp 11
    • Hóa Học Lớp 11
    • Sinh Học Lớp 11
    • Vật Lí Lớp 11
    • Lịch Sử Lớp 11
    • Địa Lí Lớp 11
    • GDCD Lớp 11
  • Lớp 10
    • Toán Lớp 10
    • Ngữ Văn Lớp 10
    • Tiếng Anh Lớp 10
    • Hóa Học Lớp 10
    • Sinh Học Lớp 10
    • Vật Lí Lớp 10
    • Lịch Sử Lớp 10
    • Địa Lí Lớp 10
    • GDKTPL Lớp 10
    • Công nghệ lớp 10
    • Tin Học Lớp 10
  • Lớp 9
    • Toán Lớp 9
    • Ngữ Văn Lớp 9
    • Tiếng Anh Lớp 9
    • Lịch sử và địa lý lớp 9
    • Khoa Học Tự Nhiên Lớp 9
    • GDCD Lớp 9
  • Lớp 8
    • Toán Lớp 8
    • Ngữ Văn Lớp 8
    • Tiếng Anh Lớp 8
    • Lịch sử và địa lý lớp 8
    • Khoa Học Tự Nhiên Lớp 8
    • GDCD 8
  • Lớp 7
    • Toán Lớp 7
    • Văn Lớp 7
    • Tiếng Anh Lớp 7
    • Lịch Sử Và Địa Lí Lớp 7
    • Khoa Học Tự Nhiên Lớp 7
  • Lớp 6
    • Toán Lớp 6
    • Văn Lớp 6
    • Tiếng Anh lớp 6
    • Lịch Sử và Địa Lí Lớp 6
    • Khoa Học Tự Nhiên lớp 6
  • Lớp 5
    • Toán lớp 5
    • Tiếng Việt Lớp 5
    • Tiếng Anh Lớp 5
    • Lịch Sử và Địa Lí Lớp 5
  • Lớp 4
    • Toán lớp 4
    • Tiếng Việt Lớp 4
    • Tiếng Anh Lớp 4
    • Lịch Sử và Địa Lí Lớp 4
  • Lớp 3
    • Toán lớp 3
    • Tiếng Anh Lớp 3
    • Tiếng Việt Lớp 3
  • Mẹo Hay
  • Tin tức
  • Liên Hệ
Tài Liệu Học Tập
No Result
View All Result
Home chính tả

Bắt tay vào làm giao diện kéo thả cùng react-beautiful-dnd

by Tranducdoan
09/07/2026
in chính tả
0
Đánh giá bài viết

Ở bài trước thì mình đã nói về cách đăng nhập. Về demo này thì mình làm to do list nên chức năng thiết yếu phải có sẽ là kẻo thả. Với việc kết hợp sử dụng thư viện frontend là reactjs thì việc kéo thả không còn quá là phức tạp. Chẹp, nhưng mà làm thủ công thì cũng khá là vất vả mà, nên mình sẽ dùng 1 package là react-beautiful-dnd (repo: https://github.com/atlassian/react-beautiful-dnd). À, và mình sẽ chỉ làm giao diện đơn giản thôi nhé, vì thẩm mỹ không phải thế mạnh nên là các bạn muốn nó đẹp hơn thì tự thêm thắt css nhé

Để cài đặt package thì các bạn chạy command:

npm install react-beautiful-dnd -save

Trước khi làm thì cùng phải clear spec chút nhỉ. Về app của mình thì người dùng có thể tạo từng danh mục riêng, và ở trong danh mục đó sẽ tạo ra được các task. Có thể kéo thả các task trong cũng danh mục và từ danh mục này sang danh mục khác. Và các danh mục cũng có thể kéo thả để thay đổi vị trí cho thuận tiện

Mục Lục Bài Viết

  1. Model
  2. Router

Model

Danh mục của mình sẽ bao gồm :

  • _id
  • title
  • order (để sắp xếp thứ tự các danh mục)
  • created_by
  • tasks (vì mình sử dụng mongoDB nên không có khóa ngoại hay relation nên mình cần lưu lại id các tasks của danh mục, chút nữa mình sẽ giải thích rõ hơn phần này)

Khi đã có cấu trúc này thì chúng ta sẽ khởi tạo Schema modelsCategory.js

const mongoose = require(‘mongoose’); const Schema = mongoose.Schema; const CategorySchema = new Schema({ _id: Schema.Types.ObjectId, title: { type: String, required: true, min: 1, max: 255 }, order: { type: Number, required: true }, created_by: { type: String, required: true, }, tasks: [ { type: Schema.Types.ObjectId, ref: ‘Task’ } ] }); module.exports = mongoose.model(‘Category’, CategorySchema);

Ở đây các bạn thấy Tasks mình để value là

[ { type: Schema.Types.ObjectId, ref: ‘Task’ } ]

có nghĩa là đây sẽ là mảng, có kiểu dữ liệu là id và model liên quan sẽ là Task

Router

const express = require(‘express’); const mongoose = require(‘mongoose’); const verifyToken = require(‘./../middlewares/verifyToken’); const Category = require(‘./../models/Category’); const Task = require(‘./../models/Task’); const router = express.Router(); router.get(‘/’, verifyToken, async (request, response) => { const userId = request.userID; const data = await Category.find({created_by: userId}).populate({ path: ‘tasks’, model: ‘Task’, options: {sort: ‘order’} }).sort(‘order’).exec(); await response.send(data); }); router.post(‘/create’, verifyToken, async (request, response) => { const category = new Category({ _id: new mongoose.Types.ObjectId(), title: request.body.title, created_by: request.body.created_by, order: request.body.order }); try { const newCategory = await category.save(); await response.send(newCategory); } catch (err) { response.status(400).send(err); } }) module.exports = router;

Đầu tiên hãy nói qua về router tạo danh mục trước, khá đơn giản, chỉ là truyền những request lên và lưu lại. Còn với router lấy ra các danh mục thì các bạn có thể thấy mình dùng populate. Hiểu nôm na thì đây mà cách mình sẽ lấy dữ liệu tới những bản liên quan, ở đây sẽ là lấy ra thông tin các tasks thuộc danh mục. Mình truyền vào 1 Object gồm path, chính là key mà bạn đã quy định ở trong model Categories, option để quy định thêm những điều kiện như limit, sort,… Nếu không muốn để bất kỳ option gì thì bạn có thể để là populate(‘tasks’).

Đừng quên bổ sung route vào app.js nhé

const categoriesRouter = require(‘./routes/categories’); app.use(‘/api/categories’, categoriesRouter);

Vậy là xong phần tạo danh mục rồi đó, bạn có thể mở postman là thêm danh mục nhé

Giờ hãy tạo giao diện kéo thả nào. Mình sẽ theo quan điểm chia để trị, do đó mình sẽ cố gắng chia nhỏ các component ra để dễ dàng quản lý hơn tasksindex.js

import React, { useEffect, useState } from ‘react’; import { connect } from ‘react-redux’; import { DragDropContext } from ‘react-beautiful-dnd’; import propsToJS from ‘../../shared/prop-to-js’; import { getCategoriesState, fetchCategories, dragAndDrop } from ‘../../redux/modules/categories’; import Column from ‘./column’; import ‘./tasks.scss’; const Tasks = (props) => { const onDragEnd = (result) => { } return ( <> <div className=’tasks’> <DragDropContext onDragEnd={onDragEnd} > </DragDropContext> </div> </> ) }; const mapStateToProps = (state) => { return { } }; const mapDispatchToProps = { }; export default connect(mapStateToProps, mapDispatchToProps)(propsToJS(Tasks));

Chúng ta sẽ sử dụng component <DragDropContext></DragDropContext> bọc ngoài vùng mà chúng ta sẽ kéo thả. Giờ mình cần fetch api để lấy dữ liệu categories

reduxmodulescategories.js

import { createAction, handleActions } from ‘redux-actions’; import { fromJS } from ‘immutable’; import { all, call, takeLatest, put } from ‘redux-saga/effects’; import axios from ‘../../shared/axios’; import handleResponse from ‘../../shared/handle-response’; import { API_URL } from ‘../../shared/config’; export const FETCH_CATEGORIES = ‘FETCH_CATEGORIES’; export const FETCH_CATEGORIES_SUCCESSFULLY = ‘FETCH_CATEGORIES_SUCCESSFULLY’; export const fetchCategories = createAction(FETCH_CATEGORIES); export const fetchCategoriesSuccessfully = createAction(FETCH_CATEGORIES_SUCCESSFULLY); const setCategories = (state, action) => state.set(‘data’, fromJS(action.payload)); const categoriesInitialState = fromJS({ data: [] }) export default handleActions({ [FETCH_CATEGORIES_SUCCESSFULLY]: setCategories }, categoriesInitialState); export const getCategoriesState = state => state.get(‘categories’).get(‘data’); export function* categoriesSagas() { yield all([ takeLatest(FETCH_CATEGORIES, fetchCategoriesFromApi) ]); } function* fetchCategoriesFromApi(action) { const {payload} = action; const response = yield call(apiFetchCategories); if (response.status === 200) { const {data} = response; const {callback} = payload; if (callback) { callback(data) } yield put(fetchCategoriesSuccessfully(data)); return; } handleResponse(response); } export function apiFetchCategories() { return axios.get(`${API_URL}/api/categories`) .then(response => response) .catch(error => error.response); }

và bên file view sẽ bổ sung thêm

const Tasks = (props) => { const { handleFetchCategories, categories, handleDragAndDrop } = props; const [categoriesState, setCategoriesState] = useState(categories); useEffect(() => { handleFetchCategories({ callback: setCategoriesState }); }, []); const onDragEnd = (result) => { } return ( <> <div className=’tasks’> <DragDropContext onDragEnd={onDragEnd} > {categoriesState.map((category) => { const tasks = category.tasks; return <Column key={category._id} category={category} tasks={tasks} />; })} </DragDropContext> </div> </> ) }; const mapStateToProps = (state) => { const tasks = {}; const categories = getCategoriesState(state); return { tasks, categories } }; const mapDispatchToProps = { handleFetchCategories: fetchCategories, handleDragAndDrop: dragAndDrop }; export default connect(mapStateToProps, mapDispatchToProps)(propsToJS(Tasks));

Lúc này thì mỗi khi load trang thì api get categories sẽ được gọi và fill dữ liệu vào state. Với mỗi categories chúng ta sẽ trả về 1 <Column> tương ứng.

columnindex.js

import React, { useState } from ‘react’; import { Collapse, Button, Modal, Form, Input } from ‘antd’; import { connect } from ‘react-redux’; import { createTask } from ‘../../../redux/modules/categories’; import { PlusOutlined, EditOutlined, DeleteOutlined } from ‘@ant-design/icons’; import { Droppable } from ‘react-beautiful-dnd’; import propsToJS from ‘../../../shared/prop-to-js’; import Task from ‘./task’; const { Panel } = Collapse; const renderExtra = (showModalCreate) => ( <> <Button> <PlusOutlined onClick={event => { showModalCreate(); event.stopPropagation(); }} /> </Button> <Button> <EditOutlined onClick={event => { event.stopPropagation() }} /> </Button> <Button> <DeleteOutlined onClick={event => { event.stopPropagation() }} /> </Button> </> ) const Column = props => { const [visible, setVisible] = useState(false); const { category, tasks, handleCreateTask } = props; const [form] = Form.useForm(); const onFinishCreateTask = () => { const dataForm = form.getFieldsValue(); const dataSubmit = { title: dataForm.title, category: category._id } handleCreateTask(dataSubmit); } const showModalCreate = () => { setVisible(true); }; const handleCloseModalCreate = () => { setVisible(false); }; return ( <> <Collapse defaultActiveKey={[category._id]} > <Panel header={category.title} key={category._id} extra={renderExtra(showModalCreate)} className=’tasks__column-panel’ > <Droppable droppableId={category._id} > {provided => ( <div ref={provided.innerRef} {…provided.droppableProps} > { tasks.map((task, index) => ( <Task key={task._id} task={task} index={index} /> )) } {provided.placeholder} </div> )} </Droppable> </Panel> </Collapse> <Modal visible={visible} title=’Create task’ onOk={handleCloseModalCreate} onCancel={handleCloseModalCreate} footer={[ <Form.Item> <Button type=’primary’ onClick={onFinishCreateTask} > Create </Button> </Form.Item> ]} > <Form form={form} name=’create_task_form’ onFinish={onFinishCreateTask} autoComplete=’off’ > <Form.Item label=”Title” name=’title’ rules={[{ required: true, message: ‘Required’ }]} > <Input /> </Form.Item> </Form> </Modal> </> ) } const mapStateToProps = state => { return {}; } const mapDispatchToProps = { handleCreateTask: createTask } export default connect(mapStateToProps, mapDispatchToProps)(propsToJS(Column));

Từng danh mục giờ sẽ được nằm trong component <Droppable> nhằm quy định vùng được thả vào, mỗi vùng này sẽ phải có một id khác nhau và id phải là string nhé (https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/identifiers.md). Đừng quên những thuộc tính như là provided.innerRef, provided.droppableProps, {provided.placeholder}

Và cuối cùng sẽ là phần chúng ta có thể kéo, đó chính là các task taskindex.js

import React from ‘react’; import { Draggable } from ‘react-beautiful-dnd’; import propsToJS from ‘../../../../shared/prop-to-js’; const Task = props => { const { task, index } = props; return ( <> <Draggable draggableId={task._id} index={index} > {provided => ( <div {…provided.draggableProps} {…provided.dragHandleProps} ref={provided.innerRef} className=’tasks__column-panel-item’ > {task.title} </div> )} </Draggable> </> ) } export default propsToJS(Task);

Với những phần tử kéo được thì chúng ta sẽ sử dụng component <Draggable>. Vậy là chúng ta đã xong được phần giao diện. Có thể kéo được nhưng khi thả thì dữ liệu vẫn giữ nguyên. Do dữ liệu của chúng ta không được render lại. Do đó ta cần thêm một function để set lại dữ liệu. Quay lại file tasksindex.js

const onDragEnd = (result) => { const { source, destination, reason } = result; if (reason === ‘DROP’ && destination && source) { const currentCategoryId = source.droppableId; const dropedCategoryId = destination.droppableId; const categoryDestination = categoriesState.find(item => item._id === dropedCategoryId); let tasksDestination = categoryDestination.tasks; const newIndex = destination.index; const taskId = result.draggableId; const newCategoriesData = sortTasksWhenDrag(tasksDestination, newIndex, result, dropedCategoryId, currentCategoryId); const data = { currentCategoryId, dropedCategoryId, taskId, newIndex, newCategoriesData }; } } const reOrderArray = (array) => array.map((item, index) => { const newItem = Object.assign({}, item, { order: index }); return newItem; }) const sortTasksWhenDrag = (tasksDestination, newIndex, result, dropedCategoryId, currentCategoryId) => { let newCategoriesData = […categoriesState]; if (dropedCategoryId === currentCategoryId) { // when drag drop in same category let newTasks = tasksDestination.filter(item => item._id !== result.draggableId); const task = tasksDestination.find(item => item._id === result.draggableId); newTasks.splice(newIndex, 0, task) newTasks = reOrderArray(newTasks); newCategoriesData = categoriesState.map(item => { if (item._id === dropedCategoryId) { const newItem = Object.assign({}, item, { tasks: newTasks.sort(sortTasks) }); return newItem }; return item; }); } else { // when drag drop in diff category const currentCategory = categoriesState.find(item => item._id === currentCategoryId); const task = currentCategory.tasks.find(item => item._id === result.draggableId); tasksDestination.splice(newIndex, 0, task); const newTasksDestination = reOrderArray(tasksDestination); newCategoriesData = categoriesState.map(item => { if (item._id === dropedCategoryId) { const newItem = Object.assign({}, item, { tasks: newTasksDestination.sort(sortTasks) }); return newItem }; if (item._id === currentCategoryId) { let newTasks = item.tasks.filter(value => value._id !== result.draggableId); newTasks = reOrderArray(newTasks); const newItem = Object.assign({}, item, { tasks: newTasks.sort(sortTasks) }); return newItem; } return item; }); } setCategoriesState(newCategoriesData); return newCategoriesData; }

Ở đây thì mình có lấy những thông tin cần thiết khi trigger sự kiện drag and drop. Sau đó mình sẽ chia ra làm 2 phần xử lý riêng biệt: vùng drop vẫn là vùng hiện tại hoặc là vùng drop là vùng mới. Với vùng hiện tại thì mình sẽ xóa phần tử drag khỏi dữ liệu, sau đó push ngược vào theo index mới trả về, sau đó cập nhập lại order theo đúng index của từng phần tử trong mảng. Với trường hợp còn lại thì xử lý cũng gần như tương tự, nhưng là xóa dữ liệu của vùng bị kéo ra, re-order lại. Tới vùng được thả vào thì sẽ push vào theo index mới rồi cũng re-order lại.

Và đây là kết quả nha: 😄

Bài viết hôm nay tới đây thôi, cảm ơn các bạn đã theo dõi

app repo: https://github.com/duongmanhhoang/reactjs-to-do

api repo: https://github.com/duongmanhhoang/demo-node-js

Previous Post

Next Post

Các dạng bài tập về Tập hợp chọn lọc có lời giải

Tranducdoan

Tranducdoan

Trần Đức Đoàn sinh năm 1999, anh chàng đẹp trai đến từ Thái Bình. Hiện đang theo học và làm việc tại trường cao đẳng FPT Polytechnic

Next Post

Các dạng bài tập về Tập hợp chọn lọc có lời giải

thời tiết miền bắc đọc sách online cm88 Socolive trực tiếp https://p789bet.biz/ cm88 com socolive https://mb66.black/ xoilactv tructiepbongda Xoilac cakhia tv Trực tiếp bóng đá 90phut f168 f168 MB66 MB66 cm88 com SC88 Socolive TV https://mb66ac.com/ Sunwin
Tài Liệu Học Tập

Copyright © 2022 Tài Liệu Học Tập.

Chuyên Mục

  • Đề Thi
  • Lớp 12
  • Lớp 11
  • Lớp 10
  • Lớp 9
  • Lớp 8
  • Lớp 7
  • Lớp 6
  • Lớp 5
  • Lớp 4
  • Lớp 3
  • Mẹo Hay
  • Tin tức
  • Liên Hệ

Tham Gia Group Tài Liệu Học Tập

No Result
View All Result
  • Đề Thi
  • Lớp 12
    • Lịch Sử Lớp 12
    • Địa Lí Lớp 12
    • Ngữ Văn Lớp 12
    • GD KTPL Lớp 12
    • Toán Lớp 12
    • Tiếng Anh Lớp 12
    • Hóa Học Lớp 12
    • Sinh Học Lớp 12
    • Vật Lí Lớp 12
  • Lớp 11
    • Toán Lớp 11
    • Ngữ Văn Lớp 11
    • Tiếng Anh Lớp 11
    • Hóa Học Lớp 11
    • Sinh Học Lớp 11
    • Vật Lí Lớp 11
    • Lịch Sử Lớp 11
    • Địa Lí Lớp 11
    • GDCD Lớp 11
  • Lớp 10
    • Toán Lớp 10
    • Ngữ Văn Lớp 10
    • Tiếng Anh Lớp 10
    • Hóa Học Lớp 10
    • Sinh Học Lớp 10
    • Vật Lí Lớp 10
    • Lịch Sử Lớp 10
    • Địa Lí Lớp 10
    • GDKTPL Lớp 10
    • Công nghệ lớp 10
    • Tin Học Lớp 10
  • Lớp 9
    • Toán Lớp 9
    • Ngữ Văn Lớp 9
    • Tiếng Anh Lớp 9
    • Lịch sử và địa lý lớp 9
    • Khoa Học Tự Nhiên Lớp 9
    • GDCD Lớp 9
  • Lớp 8
    • Toán Lớp 8
    • Ngữ Văn Lớp 8
    • Tiếng Anh Lớp 8
    • Lịch sử và địa lý lớp 8
    • Khoa Học Tự Nhiên Lớp 8
    • GDCD 8
  • Lớp 7
    • Toán Lớp 7
    • Văn Lớp 7
    • Tiếng Anh Lớp 7
    • Lịch Sử Và Địa Lí Lớp 7
    • Khoa Học Tự Nhiên Lớp 7
  • Lớp 6
    • Toán Lớp 6
    • Văn Lớp 6
    • Tiếng Anh lớp 6
    • Lịch Sử và Địa Lí Lớp 6
    • Khoa Học Tự Nhiên lớp 6
  • Lớp 5
    • Toán lớp 5
    • Tiếng Việt Lớp 5
    • Tiếng Anh Lớp 5
    • Lịch Sử và Địa Lí Lớp 5
  • Lớp 4
    • Toán lớp 4
    • Tiếng Việt Lớp 4
    • Tiếng Anh Lớp 4
    • Lịch Sử và Địa Lí Lớp 4
  • Lớp 3
    • Toán lớp 3
    • Tiếng Anh Lớp 3
    • Tiếng Việt Lớp 3
  • Mẹo Hay
  • Tin tức
  • Liên Hệ

Copyright © 2022 Tài Liệu Học Tập.