Hooks + TS build a task management system -- implementation of drag and drop function

Keywords: Javascript Front-end React TypeScript

📢 Hello, I'm Xiaocheng, a sophomore front-end enthusiast

📢 This series of articles is a learning summary of the practical jira task management system

📢 Thank you very much for reading. You are welcome to correct the wrong places 🙏

📢 May you be loyal to yourself and love life

In the last article, we wrote the task group page. Now our project has been basically completed, and all CRUD operations, route jumps and page layout have been realized. In this article, let's optimize our project again. We add a drag and drop function to my Kanban page

The content of this article is not very clear. It's a little watery. Change it after understanding it

💡 First look at knowledge points

  • Add drag and drop function to Kanban
  • Explain drop and drag in HTML5

1, Add drag and drop function to Kanban

This article only talks about one part. As the title says, add a drag and drop function

The effect is like this

We use a react beautiful DND library to realize this function. For this library, you can see: npm official website

Let's briefly introduce the use of this library. Firstly, we need to define a Droppable component to wrap our dragged elements, indicating that we can drag the contents of this area. Secondly, we need to add a Draggable component package to the place where our elements are placed, that is, to indicate that this area can be dropped

Here, the Drop and Drop components are rewritten

This part is difficult. I don't understand it very well. Let's make a few points

  • Here, we want to extract a children attribute instead of using the native children attribute
  • Due to the requirements of the API, we need to reserve receiving ref s. Here we use forwarding and forwardRef
export const DropChild = React.forwardRef<HTMLDivElement, DropChildProps>(({ children, ...props }, ref) =>
    <div ref={ref} {...props}>
        {children}
        {/* api Additional required */}
        {props.provided?.placeholder}
    </div>
)

1. Implement Drop components

// This file is equivalent to refactoring the drop native component
// Define a type. You don't want to use your own children and use your own
type DropProps = Omit<DroppableProps, 'children'> & { children: ReactNode }
export const Drop = ({ children, ...props }: DropProps) => {
    return <Droppable {...props}>
        {
            (provided => {
                if (React.isValidElement(children)) {
                    // Add props attribute to all child elements
                    return React.cloneElement(children, {
                        ...provided.droppableProps,
                        ref: provided.innerRef,
                        provided
                    })
                }
                return <div />
            })
        }
    </Droppable>
}

2. Implement Drag component

type DragProps = Omit<DraggableProps, 'children'> & { children: ReactNode }
export const Drag = ({ children, ...props }: DragProps) => {
    return <Draggable {...props}>
        {
            provided => {
                if (React.isValidElement(children)) {
                    return React.cloneElement(children, {
                        ...provided.draggableProps,
                        ...provided.dragHandleProps,
                        ref: provided.innerRef
                    })
                }
                return <div />
            }
        }
    </Draggable>
}

3. Drag persistence

Written two components, although very difficult, you can directly cv look at this part of the code.

  • It's quite understandable. Use the Drop component to Drag the package to the location, and use the Drag component to wrap the location
  • Finally, we need to persist our state, which is implemented by the onDragEnd method in the native component

We need to implement another hook to realize this function, which is very difficult

Here, we use if to determine whether it is a kanban or a task currently dragged, and whether it is left or right or up or down. We can calculate the dropped id and picked up id through the method provided in the component, and insert it into the kanban task

When we finish dragging, we will return the source and destination objects, which contain the relevant information about our dragging

If it's a column, it's a drag between kanban. We need to call a useReorderKanban method we newly encapsulated for persistence

If it is row, call the persistence method useRecordTask method between tasks for persistence

export const useDragEnd = () => {
    // Get the Kanban first
    const { data: kanbans } = useKanbans(useKanbanSearchParams())
    const { mutate: reorderKanban } = useReorderKanban(useKanbansQueryKey())
    // Get task information
    const { data: allTasks = []} = useTasks(useTasksSearchParams())
    const { mutate: reorderTask } = useReorderTask(useTasksQueryKey())
    return useCallback(({ source, destination, type }: DropResult) => {
        if (!destination) {
            return
        }
        // Kanban sorting
        if (type === 'COLUMN') {
            const fromId = kanbans?.[source.index].id
            const toId = kanbans?.[destination.index].id
            // If there is no change, return directly
            if (!fromId || !toId || fromId === toId) {
                return
            }
            // Determine where the lowered position is on the target
            const type = destination.index > source.index ? 'after' : 'before'
            reorderKanban({ fromId, referenceId: toId, type })
        }
        if (type === 'ROW') {
            // Change from + to number
            const fromKanbanId = +source.droppableId
            const toKanbanId = +destination.droppableId
            // Cross Version sorting is not allowed
            if (fromKanbanId !== toKanbanId) {
                return
            }
            // Get dragged element
            const fromTask = allTasks.filter(task => task.kanbanId === fromKanbanId)[source.index]
            const toTask = allTasks.filter(task => task.kanbanId === fromKanbanId)[destination.index]
            //
            if (fromTask?.id === toTask?.id) {
                return
            }
            reorderTask({
                fromId: fromTask?.id,
                referenceId: toTask?.id,
                fromKanbanId,
                toKanbanId,
                type: fromKanbanId === toKanbanId && destination.index > source.index ? 'after' : 'before'
            })
        }
    }, [allTasks, kanbans, reorderKanban, reorderTask])
}

4. useReorderKanban

A group of data, including the starting position, the insertion position, and whether the data is in front of or behind the insertion position, are passed in to the background interface for persistence. The useMutation used here is what I mentioned earlier, and the use methods are very skilled

// Persistent data interface
export const useReorderKanban = (queryKey:QueryKey) => {
    const client = useHttp()
    return useMutation(
        (params: SortProps) => {
            return client('kanbans/reorder', {
                data: params,
                method: "POST"
            })
        },
        useReorderKanbanConfig(queryKey)
    )
}

5. Add Drop and Drop in HTML5

When we need to set an element to drag and drop, we only need to set draggable to true

<img draggable="true">

When drag and drop is executed, ondragstart and setData() occur

Executing ondragstart will call a function, the drag function, which specifies the dragged data

function drag(event)
{
    event.dataTransfer.setData("Text",ev.target.id);
}

We need to add the data type to the drag object when we use Text here

Where to place the dragged data

By default, data / elements cannot be placed in other elements. If we need to set the allowed placement, we must block the default processing of elements.

This is done by calling the event.preventDefault() method of the ondragover event:

event.preventDefault()

drop event occurs when preventing

function drop(ev)
{
    ev.preventDefault();
    var data=ev.dataTransfer.getData("Text");
    ev.target.appendChild(document.getElementById(data));
}

Code interpretation:

  • Call preventDefault() to avoid the browser's default processing of data (the default behavior of drop events is to open as links)
  • Obtain the dragged data through the dataTransfer.getData("Text") method. This method will return any data set to the same type in the setData() method.
  • The dragged data is the id ("drag1") of the dragged element
  • Append the dragged element to the drop element (target element)

(refer to Rookie tutorial)

You can try it yourself: Online demonstration

📌 summary

  1. I've learned about how to use react beautiful DND
  2. We have a general understanding of drag persistence
  3. Learned about drop and drag in HTML5

Finally, it may not be clear enough in many places. Please forgive me

💌 If there are any mistakes or questions in the article, you are welcome to leave a message and exchange private messages

Posted by lamajlooc on Tue, 02 Nov 2021 17:08:40 -0700