最近使用React Hooks结合zarm组件库,基于js对象配置方式开发了大量的h5表单页面。大家都知道h5表单功能无非就是表单数据的收集,验证,提交,回显编辑,通常排列方式也是自上向下一行一列的方式显示 , 所以一开始就考虑封装一个配置化的页面生成方案,目前已经有多个项目基于此方式配置开发上线,思路和实现分享一下。

已经发布npm包 有兴趣的可以采用或适配自己的组件库 zarm-form-render ,不到100行代码。

使用场景

任意包含表单的h5页面(使用zarm库,或自行适配自己的库)

目标

  1. 代码实现简单和简洁
  2. 基于配置
  3. 新手上手快,无学习成本
  4. 老手易扩展和维护

写之前参考了市面上的一些方案 ,大多都是通过定义json schema方式定义一套格式, 比如阿里的form-render ,通过 JSON Schema定义表单, 有可视化编辑器配置,导出json文件,动态渲染,凡此种种,无出其外。 这里就以阿里form-render为代表,谈谈它的一些缺陷 (优点自行参考官网宣传)

form-render或思路一些缺陷

  1. 我的目标是h5, form-remder目前仅仅是pc后台表单配置方案,提供了antd和fusion主题,默认不支持h5
  2. form-render 支持扩展组件,内部维护了一个映射表 mapping, 映射了数据类型和组件的映射关系,比如antd主题各数据类型和antd组件的映射关系如下,如果我有一套自己的/第三方公司统一使用的h5组件库,需要自己去配置扩展,比较麻烦。
// 例如在antd下映射如下:
export const mapping = {
default: 'input',
string: 'input',
array: 'list',
boolean: 'checkbox',
integer: 'number',
number: 'number',
object: 'map',
html: 'html',
'string:upload': 'upload',
'string:date': 'date',
'string:dateTime': 'date',
'string:time': 'date',
'string:textarea': 'textarea',
'string:color': 'color',
'string:image': 'input',
'string:email': 'input',
'string:url': 'url',
'range:date': 'dateRange',
'range:dateTime': 'dateRange',
'*?enum': 'select',
'array?enum': 'checkboxes',
};
  1. 如果用过/开发过 form-render 等基于json schema的工具, 有一个需求处理比较麻烦, 比如表单字段联动处理, form-render 提供了有限的几个联动属性,ui:options,ui:disabled,ui:hidden等, 这样算下来,除了需要掌握form-render定义的数据类型,数据类型主题组件映射,及组件关联的各种属性,还要背下额外的联动属性,不外乎多学了一门新编程的复杂程度,所以需要可视化界面辅助编辑。
import React, { useState } from 'react';
import FormRender from 'form-render/lib/antd'; const schema = {
type: 'object',
properties: {
select: {
title: '单选',
type: 'string',
enum: ['a', 'b'],
enumNames: () => ['显示输入框', '隐藏输入框'],
'ui:disabled': (formData, rootValue) => rootValue.input1.length > 5,
'ui:widget': 'radio',
},
input1: {
title: '输入框',
description: '尝试输入超过5个字符',
type: 'string',
'ui:hidden': (formData, rootValue) => formData.select === 'b',
},
},
}; const Demo1 = () => {
const [formData, setFormData] = useState({});
return (
<FormRender schema={schema} formData={formData} onChange={setFormData} />
);
}; export default Demo1;
  1. 这种配置的json适合非开发人员快速怼出表单功能, 但是不适合开发人员开发扩展,比如我要在两个输入框中间放个图文混排的东西. (另行开发一个自定义组件挂载?)

javascript object 方案

所以以阿里form-render 等以json配置实现动态渲染的方案不能满足 简单,快速,突破,共赢的码农生存准则, 如果把json换成javascript object , 那配置和扩展的能力就不一样了, 这里我们还是借用form-render的总体思路,但是将type从string,number,boolean,array,object,html等数据类型换成Function,一个Function就是一个React组件,比如antd-mobile ,zarm 的Input组件,因为本文基于zarm, 后面都基于zarm展开(对antd-mobile ,vue等第三方移动端/pc端同样适用) ,例如下面的配置

import React, { useState, useEffect } from 'react';
import FormRenderer from 'form-render';
import { Input, Cell, Radio, Select, DateSelect, Button, Toast, Panel } from 'zarm'; export default function App() {
const [data, setData] = useState({}); const layoutData = [
{
type: Input,
label: '被保人姓名',
placeholder: '请填写',
name: 'name',
},
{
type: Radio.Group,
label: '性别',
name: 'gender',
elProps: {
type: 'button',
ghost: true,
},
items: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
],
},
{
render() {
if (!data.gender) return null;
return <Cell title="你是" description={data.gender === 'male' ? '男生' : '女生'}></Cell>;
},
},
{
type: Select,
label: '爱吃的水果',
name: 'favfood',
elProps: {
dataSource: [
{ label: 'apple', value: 'apple' },
{ label: 'banana', value: 'banana' },
],
},
}, {
type: DateSelect,
label: '出生日期',
title: '被保人出生日期',
placeholder: '请选择',
name: 'birthday',
min: '1900-01-01',
},
{
type: Input,
label: '手机号',
placeholder: '请填写',
name: 'mobile',
},
{
render() {
return <div style={{ margin: '30px 6px',}}></div>;
},
},
{
render() {
return (
<Panel title="你录入的内容">
<div style={{ margin: '10px 6px' }}>{JSON.stringify(data)}</div>
</Panel>
);
},
},
]; return (
<div>
<FormRenderer layoutData={layoutData} data={data} setData={setData} />
<Button block theme="primary" onClick={() => Toast.show(JSON.stringify(data))}>
确定
</Button>
</div>
);
}

一个表单由一个数组定义,对象type为要渲染的组件, name为收集数据的key,其他属性定义组件其他的props, 基于哪个库,则参考相关库的组件API文档配置即可,对于一些特殊的/不存在的组件我们定义render函数动态渲染,然后就没有其他的了,全部是你熟悉的React, 最终我们可以定义一个自己的FormRender 接受一个一维数组用于从上往下排列表单项( 用render 不限于表单项,你可以render任意内容), 一个data用于数据收集,一个setData用于数据更新 (源自React Hooks useState), 就这么简单,源码如下

import React from 'react';
import { Cell, Radio, DateSelect, Select } from 'zarm'; // 对于无法配置(比如自定义组件,需要根据条件显示的组件等)的情况, 请使用render方法,
// getJSON() 动态返回json
// render() 自定义render export default function FormRenderer({ layoutData, data, setData }) {
const onFiledChange = (name, value) => {
let v = value; // for Select ctrl
if (Array.isArray(value)) {
v = value.map((item) => item.value)[0];
} setData({ ...data, [name]: v });
}; const onChangeFactory = (name) => (value) => onFiledChange(name, value); return (
<div className="renderer">
{layoutData.map((item, idx) => {
if (typeof item.getJSON === 'function') {
item = item.getJSON();
}
if (typeof item !== 'object' || !item) return null;
const {
name,
type,
description,
items,
elProps = {},
cellProps = {},
render,
...props
} = item; if (typeof render === 'function') {
return render();
} let children = [];
if (Array.isArray(items) && type === Radio.Group) {
children = items.map((it, idx1) => (
<Radio value={it.value} key={idx1}>
{it.label}
</Radio>
));
} props.value = data[name];
props.onChange = onChangeFactory(name); if (type === Select) {
props.dataSource = items;
}
if (type === DateSelect || type === Select) {
props.onOk = props.onChange;
delete props.onChange;
props.onChange = elProps.onChange;
} return (
<Cell key={idx} title={item.label} description={description} {...cellProps} name={name}>
{React.createElement(type, { ...props, ...elProps }, ...children)}
</Cell>
);
})}
</div>
);
}

配置说明

import * as React from 'react';
export interface Item {
type: React.Component; // 组件类型, 比如Input 等
name: string; // key
items?: Array<any>; // dataSource
description: string; // Cell description
label?: string; // Cell title
render?: () => React.ReactNode; //自定义 render
getJSON?: () => object | null; // 动态返回Item配置
elProps?: object; // 组件的props配置 , 比如type为Input, elProps则会配置到Input
cellProps?: object; // cell props配置
}
export interface Props {
layoutData: Array<Item>; // 表单布局配置
data: object; // 数据存储,name作为key,内容为value
setData: () => void; // 数据更新
}
interface FormRenderer extends React.FC<Props> {}
declare const FormRenderer: FormRenderer;
export default FormRenderer;

上述代码效果如下

这种方式唯一缺点是无法像json一样持久化存储在数据库 ,优点是可以统一移动和pc端表单配置式开发,少了大量的样板代码和嵌套, 可以统一处理数据的存取和验证, 表单项的排列方式 。

基于react hooks,zarm组件库配置开发h5表单页面的更多相关文章

  1. 基于vue2.0前端组件库element中 el-form表单 自定义验证填坑

    eleme写的基于vue2.0的前端组件库: http://element.eleme.io 我在平时使用过程中,遇到的问题. 自定义表单验证出坑: 1: validate/resetFields 未 ...

  2. Vue 2.x折腾记 - (17) 基于Ant Design Vue 封装一个配置式的表单组件

    前言 写了个类似上篇搜索的封装,但是要考虑的东西更多. 具体业务比展示的代码要复杂,篇幅太长就不引入了. 效果图 2019-04-25 添加了下拉多选的渲染,并搜索默认过滤文本而非值 简化了渲染的子组 ...

  3. 饿了么组件库element-ui正则表达式验证表单,后端验证表单。

    前言 老是遇到一些朋友问一些element-ui组件使用相关的基础问题,因为官方文档上并没有提供所有琐碎的功能代码demo.从这里开始我会根据我实际遇到的问题记录一些常见的官方文档没有详述的功能代码, ...

  4. 如何基于 React 封装一个组件

    如何基于 React 封装一个组件 前言 很多小伙伴在第一次尝试封装组件时会和我一样碰到许多问题,比如人家的组件会有 color 属性,我们在使用组件时传入组件文档中说明的属性值如 primary , ...

  5. 基于 el-form 封装一个依赖 json 动态渲染的表单控件

    nf-form 表单控件的功能 基于 el-form 封装了一个表单控件,包括表单的子控件. 既然要封装,那么就要完善一些,把能想到的功能都要实现出来,不想留遗憾. 毕竟UI库提供的功能都很强大了,不 ...

  6. 纯小白入手 vue3.0 CLI - 2.4 - 新组件 Forms.vue 中学习表单

    vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 我的 github 地址 - vue3.0St ...

  7. Django---静态文件配置,post提交表单的csrf问题(日后细说),创建app子项目和分析其目录,ORM对象关系映射简介,Django操作orm(重点)

    Django---静态文件配置,post提交表单的csrf问题(日后细说),创建app子项目和分析其目录,ORM对象关系映射简介,Django操作orm(重点) 一丶Django的静态文件配置 #we ...

  8. Django框架之第二篇--app注册、静态文件配置、form表单提交、pycharm连接数据库、django使用mysql数据库、表字段的增删改查、表数据的增删改查

    本节知识点大致为:静态文件配置.form表单提交数据后端如何获取.request方法.pycharm连接数据库,django使用mysql数据库.表字段的增删改查.表数据的增删改查 一.创建app,创 ...

  9. 基于react hooks,antd4 配置生成表单并自动排列

    react后台项目,大多都是表单处理,比如下列4种常见1*n布局 (如果手工编码,大量的Row,Col, Form.Item的嵌套,排列,如果加上联动处理,代码将十分臃肿,不易维护) 一行一列 一行两 ...

随机推荐

  1. 树莓派 4B 入门教程

    树莓派 4B 入门教程 Raspberry Pi, Raspberry Pi 3B, Raspberry Pi 4B 树莓派 4B 入门手册 PDF Raspberry Pi Beginners Gu ...

  2. BattleBots

    BattleBots 搏茨大战 https://battlebots.com/ BiteForce https://www.youtube.com/watch?v=06lyUXuQT_Y xgqfrm ...

  3. js & bitwise-operators

    js & bitwise-operators 不用加减乘除运算符, 求整数的7倍 "use strict"; /** * * @author xgqfrms * @lice ...

  4. WebRTC 信令服务器

    WebRTC 信令服务器 node.js & V8 libuv socket.io https://socket.io/ node-static SSR https://github.com/ ...

  5. robots.txt

    robots.txt A robots.txt file tells search engine crawlers which pages or files the crawler can or ca ...

  6. taro router

    taro router https://nervjs.github.io/taro/docs/router.html bug import Taro, { Component, Config } fr ...

  7. web testing

    web testing cypress https://www.cypress.io/ https://github.com/cypress-io/cypress https://docs.cypre ...

  8. 埋点 & 数据上报 & 数据异常处理

    埋点 & 数据上报 & 数据异常处理 如何在用户关闭浏览器前面,发送请求 beforeunload unload https://developer.mozilla.org/en-US ...

  9. zsh all in one

    zsh all in one zsh https://ohmyz.sh/ # install $ sh -c "$(curl -fsSL https://raw.github.com/ohm ...

  10. js异步回调Async/Await与Promise区别 新学习使用Async/Await

    Promise,我们了解到promise是ES6为解决异步回调而生,避免出现这种回调地狱,那么为何又需要Async/Await呢?你是不是和我一样对Async/Await感兴趣以及想知道如何使用,下面 ...