likes
comments
collection
share

form-render / mobile 创建自定义嵌套 / 表格组件

作者站长头像
站长
· 阅读数 17

x-render 系列里的 form-render / form-render-mobile 是表单自动生成领域常用的工具,并且也内置了多种类型的嵌套组件和列表组件。

但是官方文档里对于如何自定义嵌套、表格组件却没有介绍,网上也搜不到类似的文章。所以这篇文章就来详细介绍下如何在 form-render 以及 form-render-mobile(下文简称 fr-mobile)中自定义嵌套和列表组件,同时也会提到 form-render 1.x 和 2.x 的实现起来的区别以及对应的源码指路。

文末会给出完整的 demo 项目仓库地址,需要的可以自取。

form-render 自定义嵌套组件

先看一个基础的嵌套组件 schema(来自官方 demo):

const schema = {
  type: "object",
  displayType: "row",
  properties: {
    obj: {
      type: "object",
      title: "卡片主题",
      description: "这是一个对象类型",
      widget: "collapse",
      column: 3,
      properties: {
        input1: {
          title: "输入框 A",
          type: "string"
        },
        input2: {
          title: "输入框 B",
          type: "string"
        },
      }
    }
  }
};

其中的 schema.properties.obj 就是一个嵌套组件,form-render 会根据它是否包含 properties 属性来决定要不要当作一个嵌套组件来渲染。而其中的 collapse 则是 form-render 内置的一个收起组件。

如果你使用的是 form-render 2.x,那自定义起来就会简单不少,创建组件,进行自定义操作后渲染 props.children,然后把这个自定义组件提供给 FormRender 的 widgets 即可

const MyCard = (props) => {
  return (
    <div style={{ padding: 16, margin: 16, backgroundColor: '#eee' }}>
      {props.children}
    </div>
  );
};

function App() {
  const form = useForm();

  return (
    <FormRender
      schema={schema}
      form={form}
      widgets={{ MyCard }}
    />
  );
}

当 form-render 根据 schema 把你这个组件识别成一个嵌套组件时,会自动的帮你把内部的表单项渲染好,然后塞到 props.children 里,所以我们的自定义组件直接渲染即可。

然后把 schema 里的 widget 改成对应的自定义组件名字(注意大小写)就行了,效果如下:

form-render / mobile 创建自定义嵌套 / 表格组件

但是很多人会遇到一个问题就是,嵌套组件内部的表单项会被包裹进一个新对象(这里是 obj)里,而不是直接平铺在 form 数据的根对象里:

form-render / mobile 创建自定义嵌套 / 表格组件

严格意义上来说,这并不是一个问题,因为 schema 里的描述语义就是这样的,你嵌套对象里的值,就应该存放在一个嵌套的对象里,平铺在根对象里反而不符合语义。

但是有时候就是会有这样的需求:我的嵌套组件只是对样式进行了修改,我希望数据结构并不会产生嵌套。因为嵌套就会造成数据处理起来会从简单的循环变成递归。

这种情况也是可以实现的,首先我们需要知道,form-render 里没什么黑魔法,数据结构的嵌套本质上还是 Form.Item 的路径嵌套实现的。所以可以自己去实现 Form.Item 来处理对应的数据层级:

const MyCard2: React.FC<any> = (props) => {
  return (
    <Form.Item label="内部组件" name="a123">
      <Input />
    </Form.Item>
  );
};

这时候就能发现,嵌套的层级已经消失了,数据如同我们期望的那样保存在根对象下:

form-render / mobile 创建自定义嵌套 / 表格组件

但是总不能我们自己再把每个表单项的 schema 处理手动实现一遍吧?确实不用,form-render 中有一个内部函数 RenderCore,顾名思义,这个函数包含了最核心的表单项渲染功能,可以通过这个路径引入到咱们的组件里来:

import RenderCore from "form-render/es/render-core";

注意!是 render-core,而不是 form-core,别写错了。

RenderCore 应该怎么用?

这个函数的用法很简单,你可以直接 ctrl 点一下看看它的类型:

interface RenderCoreProps {
    schema: any;
    rootPath?: any[] | undefined;
    parentPath?: any[] | undefined;
    [key: string]: any;
}
declare const RenderCore: (props: RenderCoreProps) => any;

我们用到的就是前三个参数,schema 是要渲染的表单项配置,rootPath 代表当前数据的存放根路径,parentPath 代表当前数据的父节点路径。

后两者的含义可能比较模糊啊,不过我们这里不用太深入,只需要知道这两者决定了你数据的存放路径,所以我们两个都传 [] 就可以把数据保存在根对象里了:

const MyCard3 = (props) => {
  return RenderCore({
    schema: props.schema,
    rootPath: [],
    parentPath: []
  });
};

很简单对吧,效果也和我们预期的一样,数据被保存到根对象之下:

form-render / mobile 创建自定义嵌套 / 表格组件

不过这里有个需要注意的地方,比如我上面的 schema 里指定了 column: 3,正常情况下应该把表单项渲染成三列,但是用 RenderCore 渲染出来的还是默认的单列样式。原因在于,RenderCore 的作用是 渲染 schema 内部的表单项,所以你需要手动把嵌套组件的 props 处理成对应的样式。


再进一步,你可能会想:现在是调用一次 RenderCore 就把内部所有表单项都渲染出来了,能不能一次只渲染一个表单项?这样就可以实现灵活更高的控制了,比如对表单项进行排序,或者每个表单项都有特定的包裹样式。

这也是可以的,做法也很简单粗暴,就是遍历一下 properties,把每个表单项都分别放在一个 object 配置项里:

import sortProperties from "form-render/es/models/sortProperties";

const itemSchema = schema?.items?.properties || {};

const fieldSchemas = sortProperties(Object.entries(itemSchema)).map(([dataIndex, item]) => {
  const fieldSchema = {
    type: 'object',
    properties: {
      [dataIndex]: item
    }
  };
});

这么一顿操作之后,其实就相当于我们得到了很多个“嵌套组件”配置项,而每个组件里只包含一个表单项。之后我们给 RenderCore 传入指定的 fieldSchemas[index] 即可实现单独渲染某个表单项,效果我们这里就不赘述了,下面讲列表的时候会用到这个操作。

sortProperties 是 form-render 内置的一个纯函数,用于生成明确的排序,推荐使用。

至此,我们已经介绍了由浅入深的几种自定义嵌套组件的方法。但是无论用哪种方法,都应该保证:在实现需求的情况下尽可能的遵守 schema 的描述

最后需要明确的是,RenderCore 是为了渲染内部的表单项才使用的,如果在你的场景里并不需要自定义嵌套组件,只是想单纯的修改数据结构的话,那么官方提供的 bind 功能会更适合你,文档链接在这里 数据转换 (xrender.fun)

form-render 自定义表格组件

然后来看看怎么自定义表格,先来看一个标准表格的 schema:

const schema = {
  type: 'object',
  displayType: 'row',
  properties: {
    list: {
      title: '活动模版',
      type: 'array',
      widget: 'TableList',
      items: {
        type: 'object',
        properties: {
          input1: {
            title: '输入框 A',
            type: 'string',
          },
          input2: {
            title: '输入框 B',
            type: 'string',
          },
          input3: {
            title: '输入框 C',
            type: 'string',
          },
        },
      },
    },
  },
};

和嵌套组件类似,但是内部的表单项配置变成了一个 items 属性,这里可以介绍下相关 schema 规则:

  • 如果 schema 包含 items 属性,就会被认为是列表组件(优先度更高)
  • 如果 schema 包含 properties 属性,就会被认为是嵌套组件

同样的,这里的 widget TableList 也是 form-render 内置的一个表格组件,其实 form-render 内置了非常多的表格,链接在这里:内置表格组件 (xrender.fun),有符合需求的就不用自己再实现一遍了。

想要自定义表格组件的流程和嵌套组件一样:创建一个组件,塞给 form-render 的 widgets 里,最后给你的 schema 配置项里指定对应的组件名即可。

我们详细看一下组件内部应该怎么实现,比较长啊,你可以跟着下面的介绍回头一步步对着代码看:

const MyList = (compProps) => {
  const {
    fields = [],
    removeItem,
    addItem,
    rootPath,
    schema,
    renderCore,
  } = compProps;
  const itemSchema = schema?.items?.properties || {};

  const { props = {} } = schema;
  const { pagination = {}, ...rest } = props;

  const paginationConfig = pagination && {
    size: 'small',
    hideOnSinglePage: true,
    ...pagination,
  };

  const columns = sortProperties(Object.entries(itemSchema)).map(([dataIndex, item]) => {
    return {
      dataIndex,
      width: item.width || FIELD_LENGTH,
      title: item.title,
      render: (_, field) => {
        const fieldSchema = {
          type: 'object',
          properties: {
            [dataIndex]: {
              ...itemSchema[dataIndex],
              fieldCol: { span: 24 },
            }
          }
        };

        return (
          <div className='fr-table-cell-content'>
            {
              renderCore({
                parentPath: [field.name],
                rootPath: [...rootPath, field.name],
                schema: fieldSchema
              })
            }
          </div>
        )
      }
    };
  });

  columns.push({
    title: '操作',
    key: '$action',
    fixed: 'right',
    align: 'center',
    width: 80,
    render: (value, record) => {
      return (
        <div>
          {!props.hideDelete && (
            <Button type='text' danger onClick={() => removeItem(record.name)}>
              删除
            </Button>
          )}
        </div>
      );
    },
  });

  return (
    <div style={{ width: '100%' }} className="fr-table-list">
      <Table
        scroll={{ x: 'max-content' }}
        columns={columns}
        dataSource={fields}
        rowKey='index'
        size='small'
        pagination={paginationConfig}
        {...rest}
      />
      <Button block style={{ marginTop: 8 }} type='dashed' onClick={() => addItem()}>
        新增
      </Button>
    </div>
  );
};

注意看这个组件里边调用 renderCore 就指定了不同 rootPath 和 parentPath。

上面这个组件实现的效果是这样的:

form-render / mobile 创建自定义嵌套 / 表格组件

核心思路其实不复杂,就是拿到把每个表单项的配置拆出来(这里就用到了上面嵌套组件里提到的渲染单个表单项),然后生成对应的表格列配置。然后把各种 props 里传递进来的配置组装一顿,塞给 Table 组件就完事了。

其实就是配置项多了点,导致代码写的比较长。

这里需要注意的是,form-render 里自己对列表组件做了处理,会通过 props 里提供诸如新增一项、删除一项、移动排序之类的方法,还帮你自动生成了很多有用的配置项,我们可以打印一个 props 看一下:

form-render / mobile 创建自定义嵌套 / 表格组件

看名字就知道是干什么的,这里就不过多介绍了。

另一个需要注意的地方是 props.renderCore,这个和上面嵌套组件里提到的 RenderCore 是一样的。只不过这里自动帮你注入进来了,更方便一些。

最后要说的是,这个自定义出来的 MyList 组件,其实就是仿照内置的 TableList 组件写的。如果你想实现其他的自定义列表,可以直接从内置列表组件里找一个类似的,然后去源码里抄一下。源码链接:

x-render/packages/form-render/src/widgets at master · alibaba/x-render · GitHub

form-render / mobile 创建自定义嵌套 / 表格组件

另外还有一个小坑,你可能实现完之后发现自己的表格长这样:

form-render / mobile 创建自定义嵌套 / 表格组件

每个单元格前面都有个 label。并且用配置项里提供的 fieldCol={{ span: 24 }}labelCol={{ span: 0 }} 也没效果。

实际上内置的表格组件也没解决这个问题,只是用样式把 label 隐藏起来了而已。直接给你 Table 组件(或者外层的 div 组件加个 class: fr-table-list 即可)

return (
  <div className="fr-table-list"> // 看这里
    <Table />
  </div>
);

背后的隐藏方式也很简单:

form-render / mobile 创建自定义嵌套 / 表格组件

fr-mobile 自定义嵌套组件

OK,讲完了 PC,再讲一下移动端怎么适配自定义组件。

其实基本流程还是一样的,声明组件、传递给 FormRender 的 widgets,然后在 schema 里提供对应的组件名。

区别可能就在于 RenderCore 的引入变了,因为我们是移动端,所以是从 form-render-mobile 取的渲染核心:

import RenderCore from 'form-render-mobile/es/render-core';

这是一个简单的自定义折叠组件:

export const MyCard = (props) => {
  return (
    <AntdCollapse
      defaultActiveKey={['1']}
    >
      <AntdCollapse.Panel
        title={
          <div style={{ fontWeight: 700 }}>{props?.schema?.title}</div>
        }
        key="1"
      >
        {RenderCore(props)}
      </AntdCollapse.Panel>
    </AntdCollapse>
  );
};

效果如下:

form-render / mobile 创建自定义嵌套 / 表格组件

可以看到,这个结构下数据还是直接存在根对象里的,如果你想实现嵌套对象的话,自己设置 RenderCore 的 parentPath 参数即可。

fr-mobile 自定义表格组件

最后是移动端的表格组件,这里其实有个坑:fr-mobile 只会渲染内置的列表组件,不会去我们提供的自定义 widgets 里查对应的组件。这就导致了你就算 schema 里配的 widget 是自己的组件名,渲染时也依旧会用内置的列表组件。

解决办法就是先递归整个 schema,把 schema.type 为 array 的改成 any。由此把列表降级成普通的表单项来使用我们自定义的表格组件。

相关代码在 render-core/index.tsx,感兴趣的可以自己研究下。

知道了这一点之后,自定义列表组件就没啥难点了,下面就是一个简单的例子,包括如何把列表项内部的表单项双排显示:

const MyList = (props) => {
  const { schema = {}, readOnly = false } = props;

  return (
    <Grid.Item span={24}>
      <Form.Array
        name={[props.id]}
        renderAdd={!readOnly ? () => (
          <span>
            添加
          </span>
        ) : undefined}
        onAdd={({ add }) => add()}
        renderHeader={({ index }, { remove }) => (
          <div>
            {schema.title && (
              <span>{schema.title} {index + 1}</span>
            )}
            {!readOnly && (
              <a onClick={() => remove(index)} style={{ float: 'right' }}>
                删除
              </a>
            )}
          </div>
        )}
      >
        {fields => fields.map(({ index, key }) => {
          return (
            <Grid columns={2} key={key}>
              {RenderCore({
                schema: schema.items,
                parentPath: [index],
                rootPath: [props.id, index]
              })}
            </Grid>
          );
        })}
      </Form.Array>
    </Grid.Item>
  );
};

效果如图:

form-render / mobile 创建自定义嵌套 / 表格组件

另外如果你遇到了列表组件之外的表单项被挤到一行里的 bug:

form-render / mobile 创建自定义嵌套 / 表格组件

把你 form-render-mobile 的版本升级到 ^1.0.7-beta.1 或者更高就解决了。

form-render 1.x 和 2.x 的区别

如果你项目中是使用的 form-render 1.x 的话,那么有几点需要注意一下。

渲染核心的功能不一致

1.x 里使用的渲染核心叫做 Core,导入链接为:

import Core from 'form-render/es/form-render-core/src/core'

其接受的参数也不一样:

<Core
  hideTitle={true}
  displayType="inline"
  key={index.toString()}
  id={child}
  dataIndex={childIndex}
/>

嵌套组件和列表组件的 props 不一样

比如列表组件中会把列表项当作一个字符串数组传递给列表组件的 children 里(数组元素即上面 Core 传参里的 child)。

如果你在用 1.x 的话,我会首先推荐你尝试升级到 2.x,这个升级并没有想象中的麻烦。不过如果你的项目太大没法升级的话,也可以比着源码照抄,这里指个路:

  • 把 x-render 项目 clone 下来,切换到 v1.14.0 tag
  • 列表组件实现(包含如何调用 Core 组件,其他的列表组件也在这里):packages\form-render\src\form-render-core\src\core\RenderChildren\RenderList\TableList.js
  • Core 组件实现:packages\form-render\src\form-render-core\src\core\index.js

示例 demo

相关示例代码已经上传到 github,链接:HoPGoldy/x-render-custom-demo (github.com),用法见 readme。