• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节实现了 Table 的物料组件,这节继续来实现 Form 组件。

创建 materails/Form/dev.tsx

import { Form as AntdForm, Input } from "antd";
import React, { useEffect, useMemo, useRef } from "react";
import { useMaterailDrop } from "../../hooks/useMaterailDrop";
import { CommonComponentProps } from "../../interface";
import { useDrag } from "react-dnd";

function Form({ id, name, children, onFinish }: CommonComponentProps) {
    const [form] = AntdForm.useForm();

    const { canDrop, drop } = useMaterailDrop(["FormItem"], id);

    const divRef = useRef<HTMLDivElement>(null);

    const [_, drag] = useDrag({
        type: name,
        item: {
            type: name,
            dragType: "move",
            id: id,
        },
    });

    useEffect(() => {
        drop(divRef);
        drag(divRef);
    }, []);

    const formItems = useMemo(() => {
        return React.Children.map(children, (item: any) => {
            return {
                label: item.props?.label,
                name: item.props?.name,
                type: item.props?.type,
                id: item.props?.id,
            };
        });
    }, [children]);

    return (
        <div
            className={`w-[100%] p-[20px] min-h-[100px] ${canDrop ? "border-[2px] border-[blue]" : "border-[1px] border-[#000]"}`}
            ref={divRef}
            data-component-id={id}>
            <AntdForm
                labelCol={{ span: 6 }}
                wrapperCol={{ span: 18 }}
                form={form}
                onFinish={(values) => {
                    onFinish && onFinish(values);
                }}>
                {formItems.map((item: any) => {
                    return (
                        <AntdForm.Item
                            key={item.name}
                            data-component-id={item.id}
                            name={item.name}
                            label={item.label}>
                            <Input style={{ pointerEvents: "none" }} />
                        </AntdForm.Item>
                    );
                })}
            </AntdForm>
        </div>
    );
}

export default Form;

和 Table 的实现方式差不多,可以拖拽 FormItem 进来,然后通过 React.Children.map 变成表单项配置,之后遍历渲染 Form.Item

注意要加上 pointerEvent:none,因为编辑时 Input 不需要输入内容

在 Page、Container、Modal 组件里支持 Form 组件的 drop:

在 componentConfig 里添加 Form 组件的配置:

Form: {
    name: 'Form',
    defaultProps: {},
    desc: '表单',
    setter: [
        {
            name: 'title',
            label: '标题',
            type: 'input',
        },
    ],
    events: [
        {
            name: 'onFinish',
            label: '提交事件',
        }
    ],
    dev: FormDev,
    prod: FormDev
},

测试下:

没啥问题。

然后我们实现 FormItem 组件:

materials/FormItem/dev.tsx

const FormItem = () => <></>;

export default FormItem;

materials/FormItem/prod.tsx

const FormItem = () => <></>;

export default FormItem;

和 TableColumn 一样,它只是用于配置的,不需要渲染啥。

在 componentConfig 里配置下:

FormItem: {
    name: 'FormItem',
    desc: '表单项',
    defaultProps: {
        name: new Date().getTime(),
        label: '姓名'
    },
    dev: FormItemDev,
    prod: FormItemProd,
    setter: [
      {
        name: 'type',
        label: '类型',
        type: 'select',
        options: [
          {
            label: '文本',
            value: 'input',
          },
          {
            label: '日期',
            value: 'date',
          },
        ],
      },
      {
        name: 'label',
        label: '标题',
        type: 'input',
      },
      {
        name: 'name',
        label: '字段',
        type: 'input',
      },
      {
        name: 'rules',
        label: '校验',
        type: 'select',
        options: [
          {
            label: '必填',
            value: 'required',
          },
        ],
      }
    ]
}

然后实现下 prod 的 Form 组件:

import { Form as AntdForm, DatePicker, Input } from "antd";
import React, {
    forwardRef,
    ForwardRefRenderFunction,
    useEffect,
    useImperativeHandle,
    useMemo,
} from "react";
import { CommonComponentProps } from "../../interface";
import dayjs from "dayjs";

export interface FormRef {
    submit: () => void;
}

const Form: ForwardRefRenderFunction<FormRef, CommonComponentProps> = (
    { children, onFinish },
    ref
) => {
    const [form] = AntdForm.useForm();

    useImperativeHandle(ref, () => {
        return {
            submit: () => {
                form.submit();
            },
        };
    }, [form]);

    const formItems = useMemo(() => {
        return React.Children.map(children, (item: any) => {
            return {
                label: item.props?.label,
                name: item.props?.name,
                type: item.props?.type,
                id: item.props?.id,
                rules: item.props?.rules,
            };
        });
    }, [children]);

    async function save(values: any) {
        Object.keys(values).forEach((key) => {
            if (dayjs.isDayjs(values[key])) {
                values[key] = values[key].format("YYYY-MM-DD");
            }
        });

        onFinish(values);
    }

    return (
        <AntdForm
            name="form"
            labelCol={{ span: 5 }}
            wrapperCol={{ span: 18 }}
            form={form}
            onFinish={save}>
            {formItems.map((item: any) => {
                return (
                    <AntdForm.Item
                        key={item.name}
                        name={item.name}
                        label={item.label}
                        rules={
                            item.rules === "required"
                                ? [
                                      {
                                          required: true,
                                          message: "不能为空",
                                      },
                                  ]
                                : []
                        }>
                        {item.type === "input" && <Input />}
                        {item.type === "date" && <DatePicker />}
                    </AntdForm.Item>
                );
            })}
        </AntdForm>
    );
};

export default forwardRef(Form);

用 React.Children.map 拿到要渲染的 formItems 信息,然后遍历渲染表单项 From.Item,根据类型渲染不同表单。

onFinish 的时候,需要对 DatePicker 的 value 做下处理,因为值是 dayjs 对象,需要 format 一下拿到字符串值。

我们还通过 forwardRef + useImperativeHandle 暴露了 submit 方法,需要在 componentConfig 里注册下:

methods: [
    {
        name: 'submit',
        label: '提交',
    }
],

并且修改 prod 为刚才写的组件。

测试下看看:

然后我们加一个按钮来触发表单提交。

我们应该能在事件处理函数里拿到传过来的 values:

在 Preview 绑定事件的时候加一下参数:

测试下:

const values = args[0];

alert(JSON.stringify(values));

点击按钮触发表单提交的动作。

表单提交触发脚本执行的动作。

我们可以再加一个发送请求的动作,根据传入的 values 来发送创建请求,之后调用 Table 的刷新方法就好了。

现在我们编辑完的画布,一刷新就没有了:

这样体验不好,我们最好做一下持久化。

这是 zustand 自带的功能,用 persist 中间件实现就行:

我们做拖拽版 todolist 那个案例的时候用过:

用了 ts + middleware 的时候,create 要换种写法。

文档的解释是为了更好的处理类型:

不影响功能。

我们加一下:

const creator: StateCreator<State & Action> = (set, get) => ({
    //...
});

export const useComponetsStore = create<State & Action>()(
    persist(creator, {
        name: "xxx",
    })
);

测试下:

这样刷新后依然保存着编辑的内容。

案例代码上传了小册仓库

总结

这节我们实现了 Form 组件,并做了 store 的持久化。

Form 组件和 Table 组件一样,通过 FormItem 来配置字段,FormItem 本身不渲染内容。

Form 暴露了 submit 方法,并且支持绑定 onFinish 事件。

我们可以通过 Button 的点击事件触发 Form 的 submit,然后给 Form 的 onFinish 事件绑定一个发请求的动作,这样就实现了提交表单保存到服务端。

至此,我们的低代码编辑器就比较完善了,物料、动作都可以根据需要自己添加。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
77.低代码编辑器:拖拽优化、Table组件
Next
79.低代码编辑器:项目总结