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

摘要

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

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

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

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

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

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

答案是不需要的。

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

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

1.初始化Form组件

在上一篇中,我们实现了很多组件。我想在项目下,新增一个组件应该已经不是一个难事了。所以我们在组件里面新增一个Form组件。

javascript 复制代码
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表单,里面如果没有元素就是空空如也的)

因为是容器类型,所以在左侧列表中,我们新建一个分组,为容器类型分组:

javascript 复制代码
  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方法:

javascript 复制代码
  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,那么就递归去生成组件。

javascript 复制代码
  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,然后遍历生成子节点即可。

javascript 复制代码
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中、

javascript 复制代码
  const selectCom = (com: ComJson) => {
    return (e: any) => {
      e.stopPropagation()
      setSelectId(com.comId);
      Store.dispatch({type: 'changeSelectCom', value: com.comId});
    }
  }

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

OK,不知道你有没有注意到,即便你选中了容器中的节点,右侧属性面板也不展示。 原因是,之前我们通过comId找节点的过程是这样的:

javascript 复制代码
      const dragCom = comList.find((item: ComJson) => item.comId === dragComId)

一旦我们加入了children的结构,comList就不是一个数组了,而是一颗树,所以我们不能通过简单的find方法,去找节点了,我们要递归去找,所以我们封装一个公共方法,来实现通过comId找对应的节点。

我们在src下新增一个utils文件夹用来保存公共方法:

nodeUtils主要用来保存和节点相关的方法。

javascript 复制代码
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框得有一个标题属性才可以。

所以我们先配置好表单的属性,

javascript 复制代码
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组件的右侧列表加一个标签属性:

javascript 复制代码
const inputAttribute: ComAttribute[] = [
  // 其他配置
  {
    label: '标签',
    value: 'label',
    type: 'input'
  },
]

现在我们设置好input框的标签属性:

那我们如何让它生效呢,回到Form组件的实现,只需要将label属性,映射到Form.Item上就行了。

javascript 复制代码
      <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,现在我们就可以设置文本框的标签属性了!!!!

最后我们把其他属性补充进来:

javascript 复制代码
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上:
github.com/TeacherXin/...
commit: 第九节: 实现Form组件并串通容器组件机制

相关推荐
Rattenking8 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull6 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
BPM_宏天低代码14 小时前
低代码 BPA:简化业务流程自动化的新趋势
运维·低代码·自动化
FinGet17 小时前
那总结下来,react就是落后了
前端·react.js
王解20 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0012 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架