目前,我们的低代码项目,已经把基本的架子搭好了。后续我们会陆续添加一些其他的功能。
如果你是第一次看到这一篇文章, 建议先看一下第一节内容:
从零实现一套低代码(保姆级教程) — 【1】初始化项目,实现左侧组件列表
如果你本身就是一个前端开发,那你一定知道,如果我们实现一个表单,正常是先有一个Form组件,然后在Form组件里面嵌套文本框之类的基础控件。
那现在我们的低代码项目好像实现不了这个效果,我们只能拖拽文本框。然后去调整它的位置,再配置属性。我们不能统一的去管理一堆文本框。
那如果我们能实现一个Form组件,组件本身提供一些属性配置。可以将Form组件下的所有文本框进行统一配置。是不是就可以了呢?
例如我们通过设置Form的组件大小,就可以统一设置里面所有文本框的组件大小。
但是现在有一个问题是什么呢,如果我们通过Form组件,将文本框给包裹起来。那Form组件里的文本框应该怎么布局呢? 现在我们的组件的布局是通过定位的方式实现的,但是如果组件外面有一个父容器。我们是否还需要使用定位去实现呢?答案是不需要的。
在容器类型里的组件,我们采用流式布局的方式,将组件进行展示。
对于表单下的组件,我们只需要将其顺其自然的排列即可。
OK,既然这样,我们就开始实现容器类型组件,Form组件。
在上一篇中,我们实现了很多组件。我想在项目下,新增一个组件应该已经不是一个难事了。所以我们在组件里面新增一个Form组件。
import { Form as AntForm} from "antd"
export default function Form(props: any) {
const { children } = props
return (
<div>
<AntForm style={{width: '400px', height: '400px', border:' 1px solid blue'}}>
{
children && children.map((item: any) => {
return <AntForm.Item>
{item}
</AntForm.Item>
})
}
</AntForm>
</div>
)
}
这里我们给这个Form表单,加一个蓝色的边框和默认高度宽度。这样方便我们在画布区查看(因为一个Form表单,里面如果没有元素就是空空如也的)
因为是容器类型,所以在左侧列表中,我们新建一个分组,为容器类型分组:
const collapseItems: CollapseProps['items'] = [
{
key: 'enterDataCom',
label: '数据录入组件',
children: renderComponent(['Input','Checkbox','Radio','Rate','Switch']),
},
// 容器类型组件
{
key: 'containerCom',
label: '容器组件',
children: renderComponent(['Form'])
},
{
key: 'otherCom',
label: '其他组件',
children: renderComponent(['Button','Icon']),
}
];
OK,当然还有一些其他的代码需要你自己补充,例如图标的引入等。现在,你可以在画布区拖入一个Form组件了。
OK,现在我们想实现出,朝Form组件中拖一个文本框,唰的一下,文本框就进去了。
那我们怎么判断,我拖拽的组件是否在Form组件里面呢?
我们可以给容器组件增加一个onDrop事件,用来处理拖拽到容器上,触发的事件,但是这么写会有一个问题。
当我拖拽到容器上时,会触发两个onDrop事件,因为之前我们给mainPart加了一个onDrop事件,所以我希望触发容器的onDrop事件时,不再触发mainPart的onDrop事件了。这一点我们可以通过阻止事件冒泡来实现。
还有一个问题,就是说当我在画布区拖拽的时候,如果像下面这种拖拽。
也会触发组件onDrop方法,但是我这个时候是希望移动Form组件,所以不希望触发组件的onDrop方法,所以要给它返回。
现在我们实现组件的onDrop方法:
const onDropContainer = (com: ComJson) => {
return (e: any) => {
const dragCom = getComById(dragComId, comList)
if(com.comType === 'Form') {
if(dragCom && dragCom !== com) {
const index = comList.findIndex((item: any) => item.comId === dragCom?.comId);
if(index > -1) {
comList.splice(index, 1)
}
if(!com.childList) {
com.childList = []
}
delete dragCom.style
com.childList.push(dragCom);
Store.dispatch({type: 'changeComList', value: comList})
e.stopPropagation()
setDragComId('')
return;
}else if(dragCom){
return;
}
let comId = `comId_${Date.now()}`
const comNode = {
comType: nowCom,
comId
}
if(!com.childList) {
com.childList = []
}
com.childList.push(comNode);
Store.dispatch({type: 'changeComList', value: comList})
e.stopPropagation()
}
}
}
这里注意一下,因为我们只实现了一个容器组件,所以就直接用Form了,后面实现其他容器组件的时候,这里会进行更改。
然后我们在修改一下return出来的返回值,我们将渲染组件单独抽出一个方法,如果组件有children,那么就递归去生成组件。
const getComponent = (com: ComJson) => {
const Com = components[com.comType as keyof typeof components];
return <div onDrop={onDropContainer(com)} key={com.comId} onClick={selectCom(com)}>
<div draggable onDragStart={onDragStart(com)} className={com.comId === selectId ? 'selectCom' : ''} style={com.style}>
<Com {...com} >
{
com.children && com.children.map(item => {
return getComponent(item)
})
}
</Com>
</div>
</div>
}
return (
<div onDrop={onDrop} onDragOver={onDragOver} onDragEnter={onDragEnter} className='mainCom'>
{
comList.map((com: ComJson) => {
return getComponent(com)
})
}
</div>
)
OK,现在子节点已经到了Form组件的children里,现在我们来到Form组件的实现,只需要从props里面拿到children,然后遍历生成子节点即可。
import { Form as AntForm} from "antd"
export default function Form(props: any) {
const { children } = props
return (
<div>
<AntForm style={{width: '400px', height: '400px', border:' 1px solid blue'}}>
{
children && children.map((item: any) => {
return item
})
}
</AntForm>
</div>
)
}
现在你可以朝Form组件拖入组件了。
但这里有一个问题,你无法选中容器内部的节点了,这是为什么呢?还是事件冒泡,因为冒泡到了最外层的容器上,所以我们要在组件的点击事件里面,禁止事件冒泡,回到mainPart中、
const selectCom = (com: ComJson) => {
return (e: any) => {
e.stopPropagation()
setSelectId(com.comId);
Store.dispatch({type: 'changeSelectCom', value: com.comId});
}
}
OK,不知道你有没有注意到,即便你选中了容器中的节点,右侧属性面板也不展示。
原因是,之前我们通过comId找节点的过程是这样的:
const dragCom = comList.find((item: ComJson) => item.comId === dragComId)
一旦我们加入了children的结构,comList就不是一个数组了,而是一颗树,所以我们不能通过简单的find方法,去找节点了,我们要递归去找,所以我们封装一个公共方法,来实现通过comId找对应的节点。
我们在src下新增一个utils文件夹用来保存公共方法:
nodeUtils主要用来保存和节点相关的方法。
import { ComJson } from "../pages/builder/mainPart";
import Store from "../store";
const getComById = (comId: string, comList: ComJson []): ComJson | undefined => {
const treeList = [...comList] || [...Store.getState().comList];
for(let i=0; i<treeList.length; i++) {
if(treeList[i].comId === comId) {
return treeList[i]
}else if(treeList[i].childList) {
treeList.push(...(treeList[i].childList|| []))
}
}
}
export {
getComById
}
然后我们把项目中,使用find方法查找节点的代码,全部改成调用该方法:
OK。当你修改完之后,在容器内部的节点,也可以通过属性面板去配置属性了。
来到and官网中的API文档,可以看到Form表单有很多属性,我们一个个配置即可:
但是有一个问题,表单有一些属性是针对于label标签的,也就是说,input框得有一个标题属性才可以。
所以我们先配置好表单的属性,
import { ComAttribute } from "../attributeMap"
const formAttribute: ComAttribute[] = [
{
label: '设置表单组件禁用',
value: 'disabled',
type: 'switch'
},
{
label: '设置组件标题',
value: 'caption',
type: 'input'
},
{
label: '文本对齐方式',
value: 'labelAlign',
type: 'select',
options: [
{
value: 'left'
},
{
value: 'right'
}
],
defaultValue: 'right'
},
{
label: '设置字段组件的尺寸',
value: 'size',
type: 'select',
options: [
{
value: 'large'
},
{
value: 'small'
},
{
value: 'middle'
}
],
defaultValue: 'middle'
},
{
label: '标题显示冒号',
value: 'colon',
type: 'switch'
}
]
export {
formAttribute
}
然后我们给input组件的右侧列表加一个标签属性:
const inputAttribute: ComAttribute[] = [
// 其他配置
{
label: '标签',
value: 'label',
type: 'input'
},
]
现在我们设置好input框的标签属性:
那我们如何让它生效呢,回到Form组件的实现,只需要将label属性,映射到Form.Item上就行了。
<AntForm style={{width: '400px', height: '400px', border:' 1px solid blue'}}>
{
children && children.map((item: any) => {
// 补充label属性
return <AntForm.Item label={getComById(item.key, Store.getState().comList).label}>
{item}
</AntForm.Item>
})
}
</AntForm>
OK,现在我们就可以设置文本框的标签属性了!!!!
最后我们把其他属性补充进来:
import { Form as AntForm} from "antd"
import { getComById } from "../../../../../utils/nodeUtils"
import Store from "../../../../../store"
export default function Form(props: any) {
const { children, disabled, labelAlign, labelWrap, size, colon } = props
return (
<div>
<AntForm
disabled={disabled}
labelAlign={labelAlign}
labelWrap={labelWrap}
size={size}
colon={colon}
style={{width: '400px', height: '400px', border:' 1px solid blue'}}
>
{
children && children.map((item: any) => {
return <AntForm.Item label={getComById(item.key, Store.getState().comList).label}>
{item}
</AntForm.Item>
})
}
</AntForm>
</div>
)
}
相关的代码提交在github上:
https://github.com/TeacherXin/XinBuilder2
commit: 第九节: 实现Form组件并串通容器组件机制