React-sortablejs-实现可视化拖拽表单设计器
前言
公司后台项目准备做一个表单设计器
,所以我先动起手预热一下。
至于为什么选择React-sortablejs
?
恰好搜到,看着简单?!
思路
官网的示例很简单,导入组件简单配置
import { useState } from 'react';
import { ReactSortable } from 'react-sortablejs';
import './index.less';
const component = [
{ id: 1, compName: '组件1' },
{ id: 2, compName: '组件2' },
{ id: 3, compName: '组件3' },
];
export default function Index() {
const [components, setComponents] = useState(component);
return (
<ReactSortable
tag={'div'}
group={{
name: 'component',
pull: true,
put: true,
}}
sort={true}
list={components}
setList={(data) => {
console.log(data);
setComponents(data);
}}
>
{components.map((item) => (
<div className={`form-sandbox__components__item`} key={item.compName}>
{item.compName}
</div>
))}
</ReactSortable>
);
}
ReactSortable
组件表示里面的子级元素children
可以被拖拽,也代表着可以从其他ReactSortable
组件中拖拽得到子级元素。ReactSortable
提供group
属性,关联多个组件的拖拽交互。
list
属性传入的数据是作为当前ReactSortable
组件内子级元素的映射,setList
则根据当前ReactSortable
组件内拖拽后整理好的子级元素的数据结构。
比如两个组件交换了位置,setList
事件回调的第一个参数就能拿到对应的组件结构
ReactSortable
还提供了onAdd、onUpdate、onRemove
事件来细分用户操作。
大体框架
一个表单设计器的大体框架,至少要2处地方使用到ReactSortable
组件
- 左侧的表单组件选择区
- 右侧的表单组件放置区
考虑到表单的元素存在嵌套形式
的子父关系
,用于实现如表单横向排序和纵向排序等效果。那么ReactSortable
组件会以嵌套组件方式,用递归
的逻辑来实现。
最终拿到这种结构的数据,很好的表现出上图的组件树结构。
核心逻辑
如何组合出上图的组件树数据?
先了解组件树结构的核心:
- itemId:当前组件的唯一标识,用于作为vdom的key。
- parentId:当前组件的父级组件的itemId,用于方便查找父级。
- nodeIndex:当前组件相对其相邻同级组件位置的数组下标,用于方便组件的插入操作。
- children:当前组件的子级组件,表达父子关系。
所以需要有一个函数把树结构转一维数组
,递归遍历
传入的组件树,每一对象添加上面的字段,然后全部push到新的数组。
// 核心函数 - 树结构转一维数组
export function mapSelected<T>(child: T[], mainParent: T): T[] {
if (!child) return [];
const tempArr = [];
function map(arr: T[], parent: T) {
arr.forEach((s: T, idx: number) => {
s.itemId = s.itemId || getFormId();
s.parentId = s.parentId || parent?.itemId || null;
// nodeIndex始终根据遍历顺序
s.nodeIndex = idx;
if (s.compType === 'wrap') {
s.children = s.children || [];
map(s.children, s);
}
tempArr.push(s);
});
}
map(child, mainParent);
return tempArr;
}
parentId、itemId可以保证组件的子父级关系,nodeIndex可以保证子组件在父级children里的具体位置。
然后再把处理好的一维数组转回为树结构
,将组件树传入ReactSortable
组件的list
属性。
核心函数 - 一维数组转回为树结构
export function arrayToTree<T>(ary: T[], root: string | null): TAtot<T> {
const result = [];
const map = {};
// eslint-disable-next-line no-restricted-syntax
for (const ia of ary) {
const { parentId: pid, itemId: id } = ia as any;
map[id] = {
...ia,
children: map[id]?.children || [],
};
const item = map[id];
if (pid === root) {
result.push(item);
} else {
if (!map[pid]) {
map[pid] = {
children: [],
};
}
map[pid].children.splice(item.nodeIndex, 0, item);
}
}
return [result, map];
}
组件点击时的onClick事件利用观察者
通知页面触发相应的操作。
index.tsx
...
// 页面中监听组件的用户操作,根据用户操作调用相应的事件
useEffect(() => {
RemoveObserver.watch((o: { nodeIndex }) => {
// 点击了组件的删除按钮,做删除操作
removeComponent(nodeIndex);
});
DisposeObserver.watch((o: SetStateAction<TEventData>) => {
// 点击组件整体,打开组件配置
toggleDispose(true);
selectDispose(o);
});
return () => {
RemoveObserver.destroy();
DisposeObserver.destroy();
};
}, []);
...
eventCover.tsx
...
// 组件外套一层div,专门收集点击操作
export default function Index({ noMask = false, children, eventData }: IEventCover) {
return (
<div
className={`form-sandbox__payground__item ${(!noMask && 'form-sandbox__payground--mask') || 'under-delete'}`}
onClick={(e) => {
e.stopPropagation();
// eventData是组件的数据
// 这里是点击了组件整体,通知页面打开组件配置页
DisposeObserver.notify(eventData);
}}
>
<div
className='form-sandbox__payground--delete'
onClick={(e) => {
e.stopPropagation();
// 这里是点击了组件的删除按钮,通知页面删除当前组件
RemoveObserver.notify(eventData);
}}
>
-
</div>
{children} // 具体的某个组件
</div>
);
}
再利用对象引用
的机制,可以直接操作对象属性,页面再调用useState来刷新组件状态。
注意事项
1、由于setList
函数的回调在组件点击、拖拽、放置时都会调用,导致嵌套的组件无法区别当前的用户操作,所以使用了ReactSortable
提供的onAdd、onUpdate、onRemove
事件来区分用户操作。
function renderChildContainer(item: ItemType) {
return (
<ReactSortable
tag={'div'}
group={{
name: 'component',
pull: true,
put: true,
}}
swap
direction={'horizontal'}
fallbackOnBody={true}
swapThreshold={1}
animation={200}
list={item.children}
onUpdate={(e: any) => {
console.log('child-onUpdate操作------------------->');
updateComponent(e, item.children, item);
}}
onAdd={(e: any) => {
console.log('child-onAdd操作------------------->');
addComponent(e, item.children, item);
}}
onRemove={(e: any) => {
console.log('child-onRemove操作------------------->');
removeComponent(e, item.children, item);
}}
// 废弃,用on-event代替
setList={() => {}}
>
{(item.children && renderFormItem(item.children)
</ReactSortable>
);
}
这三个事件回调的第一个参数里返回了当前操作的组件对象和对应的dom,newDraggableIndex和oldDraggableIndex
能知道组件的位置变化
2、 setList、onAdd、onUpdate、onRemove
等事件回调中需要修改list
的数据,必须先深拷贝
list的数据,否则会直接修改到list
,导致组件错误渲染。
奠出预览地址和仓库地址:
预览:wulibinbin.github.io/react-vite-…
有兴趣的建议拉下源码运行看看,不定时更新功能!
转载自:https://juejin.cn/post/7208534700222906423