从零实现一套低代码(保姆级教程) --- 【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组件并串通容器组件机制

相关推荐
学前端搞口饭吃1 小时前
react context如何使用
前端·javascript·react.js
GDAL1 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
低代码布道师2 小时前
少儿舞蹈小程序(14)在线预约
低代码·小程序
Dragon Wu11 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师11 小时前
React面试题
前端·javascript·react.js
木兮xg11 小时前
react基础篇
前端·react.js·前端框架
三思而后行,慎承诺13 小时前
Reactnative实现远程热更新的原理是什么
javascript·react native·react.js
知识分享小能手13 小时前
React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)
前端·javascript·vue.js·学习·react.js·前端框架·vue3
RestCloud16 小时前
低代码、无代码、iPaaS:到底有什么区别?
低代码·api
夏天199516 小时前
React:聊一聊状态管理
前端·javascript·react.js