React 2024 年 3 月 31 日
React中实现拖拽功能的自定义Hooks
React中实现拖拽功能的自定义Hooks
React中实现拖拽功能的自定义Hooks
首先要拆分拖拽列表的设计
V = f(state, props)
- 页面
- DraggableList
- Draggable
- Card
- Bar
hooks封装
- useDraggable
- 状态
- dragOver
- dragging
- dragList
- 事件
- dragStart
- dragEnd
- dragOver
- …
- 状态
useDraggableHooks的实现
import React, {useState, DragEvent} from "react";
import {CardTypeItem} from "./App8";
export enum ComponentType {
DRAGGABLE = 'DRAGGABLE',
BAR = 'BAR'
}
interface NewBarItem {
type: ComponentType,
id: number,
data?: CardTypeItem
}
interface DraggableItem {
type: ComponentType,
id: number,
data: CardTypeItem
}
function draggable(item: CardTypeItem, id: number): DraggableItem {
return {
type: ComponentType.DRAGGABLE,
id,
data: item
}
}
function insertBars(list: CardTypeItem[]): (DraggableItem | NewBarItem)[] {
let i = 0;
/*
可以这样定义data,那么NewBarItem可以不用传data,而且生成了data为undefined
这样确保了NewBarItem和DraggableItem结构上一致,
即使拖拽的item没有data,也可以生成一个占位的undefined
*/
// const NewBarItem = function (data?: CardTypeItem): NewBarItem {
// return {
// type: ComponentType.BAR,
// id: i++,
// data: data
// }
// }
const NewBarItem = function (): NewBarItem {
return {
type: ComponentType.BAR,
id: i++
}
}
return [NewBarItem()].concat(
...list.map(item => {
return [draggable(item, i++),NewBarItem()]
})
)
}
function clacChanging<T>(list:T[] , drag:number, drop:number): T[] {
list = list.slice()
const dragItem = list[drag]
// dir > 0从下往上 <0 从上往下
const dir = drag > drop ? -2 : 2
const end = dir > 0 ? drop - 1 : drop + 1
let i: number = drag
for (i; i !== end; i += dir) {
list[i] = list[i + dir]
}
list[end] = dragItem
return list
}
interface useDraggableProps {
list: CardTypeItem[];
}
export interface DraggingEventHandlers {
onDragStart: (e: DragEvent<HTMLDivElement>) => void;
onDragEnd: (e: DragEvent<HTMLDivElement>) => void;
}
export interface DroppingEventHandlers {
onDragOver: (e: DragEvent<HTMLDivElement>) => void;
onDragLeave: (e: DragEvent<HTMLDivElement>) => void;
onDrop: (e: DragEvent<HTMLDivElement>) => void;
}
interface DraggableProps {
id: number;
key: string;
dragging: null | number;
eventHandlers: DraggingEventHandlers;
}
interface DropperProps {
id: number;
dragging: null | number;
dragOver: null | number;
eventHandlers: DroppingEventHandlers;
}
export default function useDraggable(props : useDraggableProps) {
const { list } = props;
const [dragList, setDragList] = useState<(DraggableItem | NewBarItem)[]>(() => insertBars(list))
const [dragOver, setDragOver] = useState<null | number>(null);
const [dragging, setDragging] = useState<null | number>(null);
return {
dragList,
// 放置函数
createDropperProps: (id: number): DropperProps => {
return {
id,
dragging,
dragOver,
eventHandlers: {
onDragOver: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
setDragOver(id)
},
onDragLeave: (e:DragEvent<HTMLDivElement>) => {
e.preventDefault()
setDragOver(null)
},
onDrop: (e:DragEvent<HTMLDivElement>) => {
e.preventDefault()
setDragOver(null)
setDragList(list => {
return clacChanging(list, dragging!, id)
})
},
}
}
},
// 拖拽函数
createDraggerProps: (id: number, key: string): DraggableProps => {
return{
id,
key,
dragging,
eventHandlers: {
onDragStart: (e: DragEvent<HTMLDivElement>) => {
e.stopPropagation()
setDragging(id)
},
onDragEnd: (e: DragEvent<HTMLDivElement>) => {
setDragging(null)
},
}
}
}
}
}
页面里实现
import {Component, ReactNode, useState} from "react";
import './App8.css'
import cc from './images/cc.jpg'
import useDraggable, {ComponentType, DraggingEventHandlers, DroppingEventHandlers} from "./useDraggable";
export interface CardTypeItem {
src?: string;
title?: string;
}
const LIST: CardTypeItem[] = [
{
src: cc,
title: 'cc1'
},
{
src: cc,
title: 'cc2'
},
{
src: cc,
title: 'cc3'
},
]
function cls (def: string,...condition: [boolean,string][]) {
const list:string[] = [def];
condition.forEach(cond => {
if (cond[0]) {
list.push(cond[1]);
}
})
return list.join(' ');
}
interface BarProps {
id: number;
dragging: number | null;
dragOver: number | null;
eventHandlers: DroppingEventHandlers; // 使用正确的事件处理类型
}
// Draggable 组件的类型也应该对应更新
interface DraggableProps {
children: ReactNode;
eventHandlers: DraggingEventHandlers; // 使用正确的事件处理类型
dragging: number | null;
id: number;
}
function Bar({id, dragging, dragOver, eventHandlers}: BarProps) {
/**
* // 如果拖拽之后,中间CardTypeItem走了之后,
* 原本在这个CardTypeItem的上下存在的必然会各有1个Bar,
* 那么就会导致有两个Bar,所以删除第二个Bar
*/
if (dragging !== null && id === dragging + 1) {
return null
}
return (
<div
{...eventHandlers}
className={cls('draggable--bar',[dragOver === id,'dragover'])}
>
<div
className='inner'
style={{
height: id === dragOver ? '100px' : '0px'
}}
>
<h3>Bar:id:{id},dragging:{dragging},dragOver:{dragOver}</h3>
</div>
</div>
)
}
function Card ({src, title}: CardTypeItem) {
return (
<div className='card'>
<img src={src} alt=""/>
<span>{title}</span>
</div>)
}
// Draggable 组件的类型也应该对应更新
interface DraggableProps {
children: ReactNode;
eventHandlers: DraggingEventHandlers; // 使用正确的事件处理类型
dragging: number | null;
id: number;
}
function Draggable({children, eventHandlers, dragging, id}:DraggableProps) {
return (
<div
{...eventHandlers}
draggable={true}
className={cls('draggable',[dragging === id,'dragging'])}
>
<h3>dragging:{dragging},id:{id}</h3>
{children}
</div>
)
}
function App() {
return (
<div className='App'>
<div>
<DraggableList list={LIST}/>
</div>
</div>
)
}
interface DraggableListProps {
list: CardTypeItem[]
}
function DraggableList({list} :DraggableListProps) {
const { dragList, createDraggerProps, createDropperProps } = useDraggable({ list });
return (
<>
{dragList.map((item, i) => {
if (item.type === ComponentType.BAR) {
return <Bar {...createDropperProps(i)} key={item.id}></Bar>;
} else {
// 注意:这里将 item.data 作为属性传递给 Card,确保 item.data 满足 Card 组件的属性要求
// 如果 item.data 的属性可能为 undefined,应该提供默认值或者进行条件渲染
return (
<Draggable {...createDraggerProps(i, item.id.toString())} key={item.id}>
{/* 这里修改了,确保正确地传递属性给 Card 组件*/}
<Card {...item?.data}></Card>
</Draggable>
);
}
})}
</>
)
}
export default App
需要注意的点
第一次用ts来写demo
遇到几个自己不懂的点
-
DraggableItem 和 NewBarItem的类型区别 DraggableItem比NewBarItem多了一个确定性的data字段。考虑需要用一个接口还是两个接口来完善类型。 一个的话,那么在NewBarItem填入data字段的方法就可以在形参里面定义data?: CardTypeItem。
-
不一定什么东西都需要上泛型 本来想给insertBars上泛型的,但是GPT给出的建议是,传入的东西其实都有确定性的。也就是传入的东西都是可控的,那么泛型就没有必要,定义一个接口类型就可以搞定了、
-
对泛型T有更深的认知 就是对于一个你想去描述的泛型有点复杂,但是可以抽象出他的传递其实和实现并没有强联系。而是其实可以return回去。所以怎么来就可以怎么走。所以定义泛型T就可以知道不确定中的确定性。 回顾下clacChanging方法
// 这个T就是对list的描述,本身这个list可以用DraggableItem 和 NewBarItem来描述
// 但是其实没有必要,因为list作为参数,只是对list进行了重排,并没有修改其数据结构,并且也返回了这个list
// 所以没必要对这个Item有跟深的交流,而是重心放在逻辑上的处理就是
function clacChanging<T>(list:T[] , drag:number, drop:number): T[] {
list = list.slice()
const dragItem = list[drag]
// dir > 0从下往上 <0 从上往下
const dir = drag > drop ? -2 : 2
const end = dir > 0 ? drop - 1 : drop + 1
let i: number = drag
for (i; i !== end; i += dir) {
list[i] = list[i + dir]
}
list[end] = dragItem
return list
}
- 对any[]的担忧 就是ts确实容易写成anyts。因为很多时候大多数人想的是能跑就行。ts相当于是对你的道德约束。确实也不能强制你去遵守道德规范,遵守实践与否,道不远人,自在人心。
感悟
很多事情其实不要想着完全学会了才开始去实践操作。而最好的就是,just do it。干就完了。要完成一个东西,先把这个东西完成到60%,然后再去完善。这项做反馈是最有效的,并且对于自己来说,付出的性价比更高。
对于我们这些小卡拉咪来说,看源码是一种对实践的浪费。因为源代码有大量的数据结构和算法,你没有先天圣体,很难get到这样那样,作者想表达的tips。因为我们这方面的功底欠缺或者功底不够。对于我们提升最大的,更希望把重心放在其背后的思想上。
精力是有限的,肯定想让自己的效益最大化,就好比OP需要刷一个角色的练度,0~80%的练度提升是最快的,80%~100%的练度需要大量刷圣遗物来堆起来。所以提升最快的就是60%理论。听人劝,吃饱饭。饭要一口一口的吃,成长要一步一步来。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
作者: 保安 发表日期:2024 年 3 月 31 日