React中使用react-sortablejs实现自定义表单设计器,实现拖拽,排序功能
1.前言
最近做了一个自定义配置布局的需求,项目的最终实现效果如下:
需求使用的就是react-sortablejs,于是就打算简单再写个自定义表单设计器的demo,我会在下面的例子里详细的提供,各个文件的代码,让大家直接复制粘贴就能使用,以供大家参考。
2.简单介绍react-sortablejs
React-Sortablejs是一个基于React的可排序列表组件,它使用Sortable.js来实现排序功能。
使用React-Sortablejs,您可以轻松地创建可拖动和可排序的列表,并可以在React应用程序中进行自定义。它支持各种功能,例如拖动和放置,延迟拖动,动画和触摸控制。
官方api文档如下:
http://www.sortablejs.com/options.html
gitHub地址如下:
https://github.com/SortableJS/react-sortablejs
这里就不细聊各个api的作用了。有需要了解的可以自行查看。
3.自定义表单设计开发
1.组件目录
2.各个文件的代码
安装react-sortablejs
npm install react-sortablejs
1.index.js
import React, { useState } from "react";
import { Row, Col, Button } from "antd";
import { FormSetDiv } from "./style.js";
import FieldSetLeft from "./FieldSetLeft";
import FieldSetCenter from "./FieldSetCenter";
import FieldSetRight from "./FieldSetRight";
const FormFieldSet = () => {
const [currentField, setCurrentField] = useState({});
const [fieldList, setFieldList] = useState([]);
return (
<FormSetDiv>
<div className="fieldSet-header">
<Button type="primary">保存</Button>
</div>
<div className="fieldSet-main">
<Row className="fieldSet-main-content">
<Col span={3} className="fieldSet-main-content-left">
<FieldSetLeft />
</Col>
<Col span={15} className="fieldSet-main-content-center">
<FieldSetCenter
fieldList={fieldList}
currentField={currentField}
onFieldChange={(list) => setFieldList(list)}
handleChooseField={(field) => setCurrentField(field)}
/>
</Col>
<Col span={6} className="fieldSet-main-content-right">
<FieldSetRight
fieldList={fieldList}
currentField={currentField}
onFieldChange={(list) => setFieldList(list)}
handleChooseField={(field) => setCurrentField(field)}
/>
</Col>
</Row>
</div>
</FormSetDiv>
);
};
export default FormFieldSet;
2.style.js
import styled from "styled-components";
export const FormSetDiv = styled.div`
height: 100vh;
.fieldSet-header {
padding: 16px;
text-align: right;
border-bottom: 1px solid #eee;
}
.fieldSet-main {
padding: 16px 0px;
height: calc(100% - 65px);
box-sizing: border-box;
&-content {
height: 100%;
&-left {
height: 100%;
overflow: auto;
padding: 0px 16px 16px;
.title {
font-size: 16px;
margin-bottom: 12px;
}
.sort-item {
padding: 10px;
border: 1px solid #eee;
text-align: center;
margin-bottom: 12px;
border: 0.5px solid rgba(206, 210, 216, 1);
box-shadow: 0px 2px 4px 0px rgba(49, 74, 111, 0.07);
border-radius: 8px;
font-size: 14px;
color: #191c1f;
cursor: move;
&:hover {
background: #e6f1fe;
border: 0.5px solid rgba(27, 132, 251, 1);
box-shadow: 0px 2px 4px 0px rgba(49, 74, 111, 0.08);
color: #0077fa;
}
}
}
&-center {
padding: 8px 8px 15px;
background-color: #f3f6fc;
height: 100%;
overflow: auto;
box-sizing: border-box;
.field-center {
height: 100%;
overflow: auto;
background-color: #fff;
padding: 15px 12px;
box-sizing: border-box;
.main-tooltip {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #989898;
}
.sortable-chosen,
.sortable-ghost,
.sortable-drag {
cursor: move;
}
.main-field {
cursor: move;
position: relative;
padding: 16px 20px;
box-sizing: border-box;
border: 1px solid transparent;
border-radius: 8px;
margin-bottom: 8px;
&:hover {
background: rgba(255, 255, 255, 0.44);
border: 1px dashed rgba(10, 124, 251, 1);
.main-field-delete {
display: block;
}
}
&-delete {
font-size: 18px;
position: absolute;
top: 2px;
right: 2px;
color: #448aff;
cursor: pointer;
display: none;
}
.field-item {
display: flex;
align-items: center;
.field-label {
margin-right: 10px;
width: 160px;
text-align: right;
}
.field-form {
flex: 1;
}
}
}
}
}
&-right {
padding: 0px 16px;
.title {
font-size: 16px;
margin-bottom: 12px;
}
.content {
.label {
margin-bottom: 10px;
}
}
}
}
}
`;
3.FieldSetLeft.js
import React from "react";
import { ReactSortable } from "react-sortablejs";
const fieldList = [
{
fieldKey: "Input",
fieldLabel: "单行文本框",
fieldConfig: {},
},
{
fieldKey: "TextArea",
fieldLabel: "多行文本框",
fieldConfig: {},
},
{
fieldKey: "DatePicker",
fieldLabel: "日期",
fieldConfig: {},
},
{
fieldKey: "Radio",
fieldLabel: "单选框",
fieldConfig: {},
},
{
fieldKey: "Checkbox",
fieldLabel: "多选框",
fieldConfig: {},
},
];
const FieldSetLeft = () => {
return (
<>
<div className="title">基础组件</div>
<ReactSortable
list={fieldList}
animation={200}
group={{
name: "sort-field",
pull: "clone",
put: false,
}}
setList={() => {}}
sort={false}
style={{ overflow: "auto" }}
forceFallback={true}
>
{fieldList.map((item) => {
return (
<div className="sort-item" key={item?.fieldKey}>
{item?.fieldLabel}
</div>
);
})}
</ReactSortable>
</>
);
};
export default FieldSetLeft;
4.FieldSetCenter.js
import React from "react";
import { ReactSortable } from "react-sortablejs";
import { CloseCircleFilled } from "@ant-design/icons";
import Layout from "./Layout";
const FieldSetCenter = ({
fieldList = [],
currentField = {},
onFieldChange = () => {},
handleChooseField = () => {},
}) => {
//删除添加的布局字段
const handleDelete = (e, id) => {
e.stopPropagation();
const newFieldList = [...fieldList];
const curIndex = newFieldList.findIndex((item) => item.id === id);
if (curIndex !== -1) {
let deleteItem = newFieldList.splice(curIndex, 1);
if (deleteItem?.[0]?.id === currentField?.id) {
handleChooseField({});
}
onFieldChange(newFieldList);
}
};
return (
<ReactSortable
list={fieldList || []}
animation={200}
group={{ name: "sort-field" }}
setList={(list) =>
onFieldChange(
list.map((item) => ({
...item,
id: item?.id ? item?.id : new Date().getTime(),
}))
)
}
sort={true}
forceFallback={true}
className="field-center"
>
{!!fieldList?.length === 0 ? (
<div className="main-tooltip">请选择左侧的组件</div>
) : (
fieldList.map((item, index) => {
return (
<div
className="main-field"
style={{
border:
currentField?.id === item?.id &&
"1px solid rgba(28,133,252,1)",
background: currentField?.id === item?.id && "#F8FBFF",
}}
key={index}
onClick={() => handleChooseField(item)}
>
<Layout field={item} />
<CloseCircleFilled
className="main-field-delete"
style={{
display: currentField?.id === item?.id && "block",
}}
onClick={(e) => handleDelete(e, item?.id)}
/>
</div>
);
})
)}
</ReactSortable>
);
};
export default FieldSetCenter;
5.FieldSetRight.js
import React from "react";
import { isEmpty } from "lodash";
import { Input } from "antd";
const FieldSetRight = ({
fieldList = [],
currentField = {},
onFieldChange = () => {},
handleChooseField = () => {},
}) => {
const { fieldLabel } = currentField;
const handleChange = (e) => {
const val = e.target.value;
let newFieldList = [...fieldList];
const curIndex = newFieldList.findIndex(
(item) => item?.id === currentField?.id
);
if (curIndex !== -1) {
newFieldList[curIndex]["fieldLabel"] = val;
onFieldChange(newFieldList);
handleChooseField(newFieldList[curIndex]);
}
};
return (
<>
<div className="title">字段设置</div>
{!isEmpty(currentField) && (
<>
<div className="content">
<div className="label">字段名称:</div>
<Input value={fieldLabel} onChange={handleChange} />
</div>
</>
)}
</>
);
};
export default FieldSetRight;
6.Layout/index.js
import React, { useMemo } from "react";
import Input from "./Input"; //单行文本框
import TextArea from "./TextArea"; //多行文本框
import DatePicker from "./DatePicker"; //日期
import Radio from "./Radio"; //单选框
import Checkbox from "./Checkbox"; //多选框
const FIELD_MAP = {
Input,
TextArea,
DatePicker,
Radio,
Checkbox,
};
const Base = ({ field }) => {
const Component = useMemo(() => {
return FIELD_MAP[field?.fieldKey];
}, [field?.fieldKey]);
return Component ? <Component field={field} /> : null;
};
export default Base;
7.Input.js
import React from "react";
import { Input } from "antd";
const InputField = ({ field }) => {
const { fieldLabel } = field;
return (
<div className="field-item">
<div className="field-label">{fieldLabel}:</div>
<div className="field-form">
<Input readOnly placeholder="请输入" />
</div>
</div>
);
};
export default InputField;
以上代码最后的Layout文件里的组件我只写了一个Input,其余表单组件,可以自行编写。