前面四讲我们用ts + react方式实现了一个todolist,感兴趣的朋友建议去看前三篇,本期我们实现任务list移动,大家可以下载对于的Tag代码块,结合本章内容查阅,同时喜欢的朋友顺便点个赞吧,你们的支持是我源源不断的动力
1、移动todo list中item逻辑
移动列表中项目的方式逻辑是选中要移动的,托拽到另一个列表中的具体位置处,因此在这个过程中如下图所示:
(1)、其中我们需要通过下标拿到需要移动的卡片item,从原来任务列表中移除(removeItemAtIndexs);
(2)、移动过去,找到需要插入的任务列表,将卡片插入进去(insertItemAtIndex),具体如下
/**
* @description 移除指定位置元素
* @param list {Array} 任务列表
* @param i {number} 去除位置
* @returns list 列表
*/
export const removeItemAtIndex = <T>(list: T[], i: number) => {
return [...list.slice(0, i), ...list.slice(i + 1)]
}
/**
*
* @param list {array} 插入的列表
* @param item {T} 项目
* @param i {number} 插入位置
* @returns
*/
export const insertItemAtIndex = <T>(list: T[], item: T, i: number) => {
return [...list.slice(0, i), item, ...list.slice(i)]
}
这里有个ts的小知识泛型,如果有一个函数需要可能去操作不同类型的数据,但是思路方式又是相同,我们就可以通过泛型的方式去定义这样的泛型函数,具体使用泛型如下:
// 泛型interface
interface ItemInfo<T> {
children: T
}
// 泛型type
type Item<T> = {
children: T
}
// 在使用的过程中我们可以通过
let info: Item<Array<string>> = { children: ["泛型示例"] }
对于泛型函数,我们还可以用这么用:
function demo<T = Record<string, any>>(item: T, key: keyof T) {
return item[key]
}
const info = { name: '123', msg: '这是一个测试内容' }
demo(info, 'msg')
这样结合泛型,我们在项目中的语法提示就会根据数据类型进行智能提示,如下图所示;可以避免一些错误,同样很方便开发;
2、移动卡片实现
上面我们讲了,基本去实现一个数组列表操作的移除插入的操作,这是我们实现托拽的一个基本思路,下面介绍一下我们托拽移动卡片的需求,如下图所示:
托拽功能呢我们需要借助react两个常用的开源库,具体安装如下:
yarn add react-dnd react-dnd-html5-backend
具体如何使用大家可以去参考官方文档,本节暂时不做托拽库的使用介绍,附上官方文档;
下面我们设计一下功能的流程:
针对于上述流程结合我们代码,程序这样去设计
下面是我的实现方法:
- 第一步: 定义draggeditem信息存储模块
// 托拽 type.ts定义
....
....
/**
* @description 托拽的项目
*/
export interface DragItem {
id: string
text: string
type: 'COLUMN'
}
export interface AppState {
/**
* @description 板块list
*/
lists: List[]
/**
* @description
*/
draggedItem: DragItem | null
}
export type Action = {
type: 'ADD_LIST'
payload: string
} | {
type: 'ADD_TASK'
payload: { text: string, listId: string }
} | {
type: "MOVE_LIST"
payload: { dragId: string, hoverId: string }
} | {
type: 'SET_DRAG_ITEM',
payload: DragItem | null
}
// AppAction.ts
export const setDragedItem = (item: DragItem | null): Action => ({
type: 'SET_DRAG_ITEM',
payload: item
})
- 第二步:通过
AppStateContext为所有Todo模块提供draggedItem托拽的卡片信息;
import React, { createContext, useContext, Dispatch } from 'react'
import { List, Task, AppState, Action, DragItem} from './type'
import { useImmerReducer } from 'use-immer'
import { appStateReducer } from './Reducer'
export interface AppStateContextProps {
lists: List[]
getTasksByListId(id: string): Task[]
dispatch: Dispatch<Action>
draggedItem: DragItem | null
}
const AppStateContext = createContext<AppStateContextProps>(
{} as AppStateContextProps
)
const appData: AppState = {
lists: [
{
id: "0",
text: "待处理",
tasks: [{ id: "c0", text: "我想去新疆天山" }]
},
{
id: "1",
text: "进行中",
tasks: [{ id: "c2", text: "不好意思居家隔离了" }]
},
{
id: "2",
text: "已完成",
tasks: [{ id: "c3", text: "车票已经作废了" }]
}
],
draggedItem: null
}
export const AppStateProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
// 创建我们子组件中用到的数据
const [todoInfo, dispatch] = useImmerReducer(appStateReducer, appData)
const { lists, draggedItem } = todoInfo
const getTasksByListId = (id: string) => {
return lists.find((i: List) => i.id === id)?.tasks || []
}
return (
<AppStateContext.Provider value={{ ...draggedItem, ...todoInfo, getTasksByListId, dispatch}}>
{children}
</AppStateContext.Provider>
)
}
export const useAppState = () => {
return useContext(AppStateContext)
}
- 第三步: 增加修改draggedItem的分支方法
...
case "SET_DRAG_ITEM":
drap.draggedItem = action.payload;
break;
...
- 第四步:在卡片组件中,我们通过react-dnd库为卡片主项目正价托拽功能,具体实现如下
import { useDrag } from "react-dnd";
import { setDragedItem } from "../../state/TodoState/Appction";
import { useAppState } from "../../state/TodoState/AppStateContext";
import { DragItem } from "../../state/TodoState/type";
/**
* @description 托拽hooks
*/
export const useDragItem = (item: DragItem) => {
const { dispatch } = useAppState()
const [, drag] = useDrag({
type: item.type,
item: () => {
dispatch(setDragedItem(item))
return item
},
end: () => dispatch(setDragedItem(null))
})
return { drag }
}
import { ColumnContainer, ColumnTitle } from "../../styles"
import Card from "./Card"
import { useAppState } from "../../state/TodoState/AppStateContext"
import { Task } from "../../state/TodoState/type"
import AddNewItem from "./AddNewItem"
import { addTask, moveList } from "../../state/TodoState/Appction"
import { useRef } from "react"
import { useDragItem } from "./hooks"
import { useDrop } from "react-dnd"
import { throttle } from "lodash"
type ColumnProps = {
text: string
id: string
}
const Column: React.FC<ColumnProps> = ({ text, id }) => {
const { draggedItem, getTasksByListId, dispatch } = useAppState()
const tasks = getTasksByListId(id)
const ref = useRef<HTMLDivElement>(null)
// 获取执行托拽的项目
const { drag } = useDragItem({type: 'COLUMN', id, text})
// 放下托拽内容
const [, drop] = useDrop({
accept: "COLUMN",
hover: throttle(() => {
// 如果没有托拽项目就不执行下放
if(!draggedItem) {
return
}
// 如果是行托拽就执行
if (draggedItem.type === 'COLUMN') {
// 如果是托拽内容还是在本列表内就不执行下放操作
draggedItem.id !== id && dispatch(moveList(draggedItem.id, id))
}
}, 150)
})
// 执行drag drop
drag(drop(ref))
return (
<ColumnContainer ref={ref}>
<ColumnTitle>{text}</ColumnTitle>
{
tasks.map((i: Task) => (
<Card id={i.id} text={i.text} key={`${i.id}${i.text}`} />
))
}
<AddNewItem
toggleButtonText='添加下一个任务'
onAdd={(text: string) => dispatch(addTask(text, id))}
></AddNewItem>
</ColumnContainer>
)
}
export default Column
这样我们就实现了一个卡片托拽版本的todolist,大概实现思路在这里,代码分支tag我已上传到了gitee,感兴趣的朋友可以去下载本章的tag分支查阅,如果对你有帮助,转评赞三连随便给一个~谢谢你们的支持。