从零实现一套低代码(保姆级教程) --- 【9】实现Form组件并串通容器组件机制

发布时间:2023年12月28日

摘要

目前,我们的低代码项目,已经把基本的架子搭好了。后续我们会陆续添加一些其他的功能。

如果你是第一次看到这一篇文章, 建议先看一下第一节内容:
从零实现一套低代码(保姆级教程) — 【1】初始化项目,实现左侧组件列表

如果你本身就是一个前端开发,那你一定知道,如果我们实现一个表单,正常是先有一个Form组件,然后在Form组件里面嵌套文本框之类的基础控件。

那现在我们的低代码项目好像实现不了这个效果,我们只能拖拽文本框。然后去调整它的位置,再配置属性。我们不能统一的去管理一堆文本框

在这里插入图片描述

那如果我们能实现一个Form组件,组件本身提供一些属性配置。可以将Form组件下的所有文本框进行统一配置。是不是就可以了呢?

例如我们通过设置Form的组件大小,就可以统一设置里面所有文本框的组件大小。

但是现在有一个问题是什么呢,如果我们通过Form组件,将文本框给包裹起来。那Form组件里的文本框应该怎么布局呢? 现在我们的组件的布局是通过定位的方式实现的,但是如果组件外面有一个父容器。我们是否还需要使用定位去实现呢?

答案是不需要的。

在容器类型里的组件,我们采用流式布局的方式,将组件进行展示。
对于表单下的组件,我们只需要将其顺其自然的排列即可。
在这里插入图片描述

OK,既然这样,我们就开始实现容器类型组件,Form组件。

在这里插入图片描述

1.初始化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组件了。

在这里插入图片描述

2.实现向容器组件中拖入组件

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>
  )

3.在Form中对子节点进行渲染

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});
    }
  }

4.封装全局方法,通过ID找节点

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。当你修改完之后,在容器内部的节点,也可以通过属性面板去配置属性了。

在这里插入图片描述

5.配置Form的属性面板

来到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组件并串通容器组件机制

文章来源:https://blog.csdn.net/weixin_46726346/article/details/135264905
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。