likes
comments
collection
share

一篇文章说清楚低代码form表单平台 -- form内核原理

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

前言

我们的目标是将schema => form表单,schema就是一个json对象,我们看看阿里的form-render库(一个低代码reactform表单库)的schema长什么样子:

{
  "type": "object", 
  "properties": {
    "count": {
      // 基础属性
      "title": "代号",
      "type": "string",
      "min": 6,
      // rules (补充校验信息)
      "rules": [
        {
          "pattern": "^[A-Za-z0-9]+$",
          "message": "只允许填写英文字母和数字"
        }
      ],
      // props (补充antd组件props)
      "props": {
        "allowClear": true
      }
    }
  }
}

虽然官方网站说这个JSON, 遵循JSON Schema 国际规范,但是我觉得这个规范太麻烦了,我是按照ant-design的使用习惯来定义schema的,主要是更符合使用习惯,类似ant是这样使用组件的,vue的elementUI好像也是类似的用法:

<Form 这里可以定义Form的属性>
    <Form.Item name="account" 这里可以定义Form.Item的属性>
        <Input 这里可以定义表单组件的属性 />
    </Form.Item>
    <Form.Item name="password">
         <Input 这里可以定义表单组件的属性 />
    </Form.Item>
</Form>

所以对应跟组件使用差不多的schema定义如下:

{
    // 相当于在上面的 Form组件上定义的属性
    formOptions:{
        // 当字段被删除时保留字段值
        // preserve:true
    }, 
    formItems: [ // 相当于Form组件里所有Form.Item组件
      {
        // 这个属性太重要了,必填,相当于每一个组件的标识符,可以是数组
        // 数组可以是字符串或者数字,以此定义嵌套对象,嵌套数组
        name: 'account', 
        // value: '', 这里可以定义初始值,也可以不设置
        options: { // 相当于Form.Item组件属性
           // hidden: xx 隐藏表单逻辑
        }, 
        // 布局属性,后续会用这些属性控制组件的布局
        // 布局属性就是设置一行几列表单,表单label宽高等等ui属性
        // 可以看到我们是把ui属性和逻辑上表单属性解耦了的
        // 本篇文章不会涉及到这个属性
        layoutOptions: { // 留给后面拓展的布局组件属性
            // label: xx
        }, 
        // 组件名,这里'input'会被转化为ant的Input组件
        // 会有一个map将字符串转换为组件
        Wiget: 'input',
        WigetOptions: {}, // 表单组件属性
      },
    ],
}

  • 上面的name因为可以定义为数组,比如['a', 'b'],所以对应form表单的{a : { b: '更改这里' }}

  • 还可以定义为[a, 1],会被解析为更改{ a: [ undefined, '更改这里' ] },

通过这个name的命名设置,我们可以看到,既能满足数组嵌套,也能满足对象嵌套,所以可以满足几乎全部表单对象值的格式要求。

所以我们希望form内核大概使用的方式是:

// 定义schema
const schema = {
  formItems: [
    {
      name: 'account',
      value: 1,
      options: {
      },
      Wiget: 'input'
    }
  ]
}

const Demo = () => {
  const [form] = useForm({ schema });
  return <Form form={form} />;
};

ReactDOM.render(
  <Demo />,
  document.getElementById('app')
);

以上配置就渲染一个Input的组件,并且form提供一系列方法就像ant一样,可以getFiledsValue, setFieldsValue等等方法,让我们的使用跟ant几乎是无缝连接,

有人会说,直接用ant就可以改装啊,但是你要知道,

但是ant本身一些属性是函数,JSON上是不能挂函数的,因为JSON.stringify会把函数过滤掉,所以,很多ant属性需要挂函数,内部就不支持了,比如onFinish事件,shouldUpdate方法等等

还有如果我们业务某个产品需要很多自定义的需求,可能涉及到要改底层的form库,就需要自己开发一套了,所以魔改antform不太好,还不如自己开发一套呢

废话不多说,开始编码!

大体架构

我们的大体架构如下(没有写form渲染器器(即可视化拖拽表单这块功能)后续加):

一篇文章说清楚低代码form表单平台 -- form内核原理

上图比较简陋,我们先把FormStore搭建好,毕竟它是调度组件的老大,为了省时间,就不用ts了,先js跑通。

下面是使用ramda库提供的一些工具函数以及标识符,这个不重要,看函数名就能猜到这些函数什么意思了,用到的话,会具体解释这些函数的作用

import { path, clone, assocPath, merge, type, equals } from 'ramda'

// 以下是一些标识符
// 此标识符意味着通知所有组件更新
const ALL = Symbol('*');
// 此标识符用来标识formStore
const FORM_SIGN = Symbol('formStoreSign');
// 导出内部方法的标识符
const INNER_HOOKS_SIGN = Symbol("innerHooks");

FormStore

用于存放表单数据、接受表单初始值,以及封装对表单数据的操作。

class FormStore {
  // 参数是初始化的values
  constructor(initialValue) {
    // 后续有resetValue,也就是重置表单的方法,所以要留住它
    this.initialValue = initialValue
    
    // values存储form表单的值
    // clone是ramda提供的深克隆功能
    this.values = initialValue ? clone(initialValue) : {}
    
    // 事件收集器,订阅的事件(函数)都存放在这里
    this.listeners = []
  }
}

这里表单数据仓库FormStore和每一个Form.Item(用来包裹表单比如Input组件,把Input注册到FormStore里)采用的通信方式是发布订阅模式。,在FormStore中维护一个事件回调列表listeners,每个Field创建时,通过调用FormStore.subscribe(listener)订阅表单数据变动

  // 通知的方法,通知单个或者所有组件更新表单值
  notify = (name) => {
    for (const listener of this.listeners) listener(name)
  }

  // 订阅事件的方法,返回清除事件的函数,在组件卸载的时候需要清除这个组件订阅的事件
  subscribe = (listener) => {
    this.listeners.push(listener)
    return () => {
      // 取消订阅
      const index = this.listeners.indexOf(listener)
      if (index > -1) this.listeners.splice(index, 1)
    }
  }

上面需要注意的是:

  • this.notify(name)中的的name,可以是数组或者字符串,比如['account', 'CCB'], ['account', 0]

再添加getFieldValuesgetFieldValuesetFieldValue, setFieldsValue函数,作用分别是:

  • getFieldValues:获取整个表单项的值
  • getFieldValue:获取单个表单项的值
  • setFieldValue:设置单个表单项的值,其中调用notify(name),通知单个表单更新
  • setFieldsValue: 设置多个表单项的值,其中调用notify(name),以保证所有的表单变动都会触发通知
  // 获取表单值
  getFieldValues = (name) => {
    return clone(this.values)
  }

  // 这里的name不一定是字符串,也有可能是字符串数组,或者数组下标(string | string | number[])
  //  例如:name = ['a', 'b']意思是获取form表单值(value)对象的value[a][b]属性值
  getFieldValue = (name) => {
    if (typeof name !== 'string' && !Array.isArray(name)) {
      throw new Error(`参数 ${name} 需要是字符串或者数组`)
    }
    // strToArray定义在下面,就是转化为数组的函数
    // 因为path第一个参数必须是数组,name有可能是字符串
    // path用法:
    // path(['a', 'b'], {a: {b: 2}}) => 2
    return path(strToArray(name), this.values)
  }

  // 设置form表单 单个值的方法
  setFieldValue = (name, value) => {
    const newName = strToArray(name)
    // assocPath是ramda用来给对象设置值的函数
    // assocPath用法:
    // assocPath(['a', 'b', 'c'], 42, {a: {b: {c: 0}}})
    // => {a: {b: {c: 42}}}
    this.values = assocPath(newName, value, this.values)
    // 发布事件,我们的事件都是以名字字符串作为标识
    this.notify(name)
  }

  // 设置form表单 多个值的方法
  setFieldsValue = (value) => {
    // 如果value不是对象({}这样的对象,其它数组这些对象不行,此函数不执行
    if (R.type(value) !== 'Object') return
    // pickPath方法能把对象解析为路径
    // pickPaths({a: 2, c: 3 })
    // => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
    const paths = pickPaths(value)
    paths.forEach((item) => {
      this.values = assocPath(item.path, item.value, this.values)
    })
    this.notify(ALL)
  }

然后还有一些工具函数以及导出的函数,功能和作用都写在注释里,这样FormStore组件大致完成。

 // 暴露formStore的内部方法给外面,不让其直接访问FormStore
  getFormExport = (schema) => {
    return {
      signType: FORM_SIGN,
      getFieldValue: this.getFieldValue,
      setFieldValue: this.setFieldValue,
      setFieldsValue: this.setFieldsValue,
      isSamePath: this.isSamePath,
      getInnerHooks: this.getInnerHooks(schema),
    }
  }
  // 判断两个路径是否相等,如下
  // equals([1, 2, 3], [1, 2, 3]); //=> true
  isSamePath = (path1, path2) => {
    if (type(path1) !== 'Array' || type(path2) !== 'Array') {
      throw new Error(`isSamePath函数的参数均需数组`)
    }
    return equals(path1, path2) //=> true
  }
  
  // 获取内部方法,只在内部组件使用
  getInnerHooks = schema => sign => {
    if(sign === INNER_HOOKS_SIGN) {
      return {
        getFieldValue: this.getFieldValue,
        setFieldValue: this.setFieldValue,
        setFieldsValue: this.setFieldsValue,
        isSamePath: this.isSamePath,
        subscribe: this.subscribe,
        notify: this.notify,
        schema
      }
    }
    console.warn('外部禁止使用getInnerHooks方法');
    return null;
  }
// 下面是工具函数

// 此函数就是把字符串转数组的函数
const strToArray = (name) => {
  if (typeof name === 'string') return [name]
  if (Array.isArray(name)) return name
  throw new Error(`${name} 参数必须是数组或者字符串`)
}

// 这个函数是用来提取对象的路径的比如说:
// pickPaths({a: 2, c: 3 })
// => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
// pickPaths({ b:[ { a : 1 } ] )
// => [[ { path: [ "b", 0, "a"], value: 1 }]]
function pickPaths(root, collects = [], resultPaths = []) {
  function dfs(root, collects) {
    if (type(root) === 'Object') {
      return Object.keys(root).map((item) => {
        const newCollect = clone(collects)
        newCollect.push(item)
        return dfs(root[item], newCollect)
      })
    }
    if (type(root) === 'Array') {
      return root.map((item, index) => {
        const newCollect = clone(collects)
        newCollect.push(index)
        return dfs(item, newCollect)
      })
    }
    return resultPaths.push({ path: collects, value: root })
  }
  dfs(root, collects)
  return resultPaths
}

好了,我们可以试试我们刚才写的的FormStore组件能干啥了

const formStore = new FormStore({ account: [ { name: 'CCB' } ] });
formStore.setFieldsValue({ account: [ { name: 'xiaoming' }, 123 ] });

// 打印formStore.value
// => { account: [ { name: 123 }, 123 ] }
console.log(formStore.values)


formStore.setFieldValue([ 'account', 1, 'age' ], 10)
// =>  { account: [ { name: 123 }, age: 10 ] }
console.log(formStore.values)
  • 上面可以看到,这个路径解析模块对我们来说非常重要,所以后续我会把它单独提取出来作为一个服务,我们在平时的业务代码里,也需要把这些比较重要的模块,单独提取成服务类,或者hooks

  • 其次后面会用函数式写法再重构一下具体的函数。上面的写法只是为了不了解函数式和不会使用ramda库的同学看。

我们接着再简单试一下formStore的注册函数功能

const formStore = new FormStore({ account: [{ name: "CCB" }] });
formStore.subscribe((name)=>{ 
   if(name === ALL || formStore.isSamePath(name, [ 'account', 0, 'name' ])){
   console.log('路径匹配 [ account, 0, name ]')
   }
})
 //  formStore.setFieldsValue({ account: [{ name: "A" }] })
 // => 打印 路径匹配 [ account, 0, name ]
 formStore.setFieldValue([ 'account', 0, 'name' ], 'A')

好了,这个模块按道理我的测试用例需要用测试库的,这里就不用了,欢迎过两天大家去看我的即将发布的jest入门。(主要是为了宣传这个,不停的学习,棒棒哒😄)

上面subscribe订阅事件和notify发布事件是一个简单的发布订阅模型。说白了跟redux的源码差不多,订阅事件就是把订阅的函数放到一个数组,发布事件就是把数组里的函数拿出来调用一遍。

接下来我们看看Form组件是怎样的,Form组件相当简单,也只是为了提供一个入口和传递上下文。

Form组件

props接收一个FormStore的实例(这个实例通过useForm({ schema })产生,后面会讲这个useForm是怎么实现的),并通过Context传递给子组件(即Field)中

import { INNER_HOOKS_SIGN } form './utils';
import { FormContext } from './context';

// form组件映射关系
const WigetsMap = {
  input: Input
}

function Form(props) {
  if (props.form.signType !== FORM_SIGN) throw new Error('form类型错误');
  // 这里的form是后面useForm产生的对象
  // 这个对象实际是formStore的exportForm方法导出的对象
  // signType用来标示是我们的formStore.exportForm方法导出的对象
  if(form.signType !== FORM_SIGN) throw new Error('form类型错误');
  // 外部传的form
  const { form, ...restProps } = props;
  // 获取到fromStore的getInnerHooks方法导出的内部函数
  const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
  
  return (
    <form
      {...restProps}
      onSubmit={(event) => {
        event.preventDefault();
        event.stopPropagation();
        // 调用了formInstance 提供的submit方法
        // innerForm.submit();
      }}
    >
      {/* formInstance 当做全局的 context 传递下去 */}
      <FormContext.Provider value={innerForm}>
        {/* useForm的时候schema会传给form */}
        {innerForm.schema?.formItem?.map((item, index) => {
          return (
             {/* formItem属性在传递给下面 */}
            <FormItem key={index} name={item.name} {...item.options}>
              {/* WigetOptions属性在传递给下面 */}
               {WigetsMap[item.Wiget] ? <item.Wiget {...item.WigetOptions} /> : null}
            </FormItem>
          );
        })}
      </FormContext.Provider>
    </form>
  );
}

getInnerHooks

Form组件主要的功能就是把innerForm传递给Form.Item组件,这个innerFrom我们看上面的FormStore组件getInnerHooks是怎么样的:

        // 获取内部方法,只在内部组件使用
        getInnerHooks = schema => sign => {
          if(sign === INNER_HOOKS_SIGN) {
            return {
              getFieldValue: this.getFieldValue,
              setFieldValue: this.setFieldValue,
              setFieldsValue: this.setFieldsValue,
              isSamePath: this.isSamePath,
              subscribe: this.subscribe,
              notify: this.notify,
              schema,
            }
          }
          console.warn('外部禁止使用getInnerHooks方法');
          return null;
        }

可以看到导出的对象必须传入INNER_HOOKS_SIGN标识符才能获取,INNER_HOOKS_SIGN是组件内部的,外面使用useForm的开发者是拿不到的,所以道处对象只服务于组件内部。

目的就是用来获取和设置属性,已经订阅和发布事件。

上文还有FormContext这个context,我们看下这个文件长什么样

FormContext

import React from 'react'

const warningFunc: any = () => {
    console.warn(
    'Please make sure to call the getInternalHooks correctly'
    );
  };
  
  export const FormContext = React.createContext({
    getInnerHooks: () => {
      return {
        getFieldValue: warningFunc,
        setFieldValue: warningFunc,
        setFieldsValue: warningFunc,
        isSamePath: warningFunc,
        subscribe: warningFunc,
        notify: warningFunc,
      };
    },
  });

默认的参数就是我们在FormStore定义的getInnerHooks的方法,保证它们两个函数导出属性名字一致,这里就体现了typescript的重要性了。

接下来,我们看一下,外部的useForm是怎么使用的

useForm

const useForm = (props) => {
  // 检测schema是否符合规范,不符合报错
  checkSchema(props.schema);
  // 保存schema的值
  const schemaRef = useRef(props.schema);
  // 保存form的引用对象
  const formRef = useRef();
  
  // 第一次渲染初始化formStore
  if (!formRef.current) {
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  // 如果schema发生变化,formStore重新生成
  if (JSON.stringify(props.schema) !== JSON.stringify(schemaRef.current)) {
    schemaRef.current = props.schema;
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  return [formRef.current];
};

// 工具函数
function checkSchema(schema) {
  ifElse(
    isArrayAndNotNilArray,
    forEach(checkFormItems),
    () => { throw new Error('formItems property of schema need to an Array') }
  )(path(['formItems'], schema));
}

function checkFormItems(item) {
  if (!all(equals(true))([isObject(item), isNameType(item.name)])) {
    throw new Error('please check whether formItems field of schema meet the specifications');
  }
}

上面唯一指值得一说的就是useRef的使用,可以当做单例模式来用,如下:

const a = useRef();
if(!a.current) return 1;
return a.current

第一次赋值1,如果存在就一直是1,不会变

接着我们看一下Form.Item组件的代码

Form.Item

import React, { cloneElement, useEffect, useContext, useState } from 'react'
import { FormContext } from './context';
import { ALL } form './utils';

function FormItem(props: any) {
  const { name, children } = props;

  // 这个是获得store的Context,后面会有讲解
  const innerForm = useContext(FormContext);

  // 如果如果我们schema初始化有值,就会传到这里
  const [value, setValue] = useState(name && store ? innerForm.getFieldValue(name) : undefined);

  useEffect(() => {
    if (!name || !innerForm) return;
    // 判断n如果是ALL表示大家都要更新
    // 或者单独更新这个form表单
    // 要求n和name相同
    return innerForm.subscribe((n) => {
      if (n === ALL || (Array.isArray(n) 
      && innerForm.isSamePath(n, name))) {
        setValue(store.getFieldValue(name));
      }
    });
  }, [name, innerForm]);

  return cloneElement(children, {
    value,
    onChange: (e) => {
      innerForm.setFieldValue(name, e.target.value);
    },
  });
}

这里需要注意的是,cloneElement把children包装了一下,传入了value和onChange方法,例如:

<Form.Item name="account" 这里可以定义Form.Item的属性>
    <Input 这里可以定义表单组件的属性 />
</Form.Item>

这里的Input就能自动接收到value和onChange属性和方法了

  • 并且onChange方法会调用innerForm的setFieldValue方法
  • 这个方法就会调用formItem在useEffect里面注册的方法,实现单独更新组件的目标,不用全局刷新

这篇文章完全是自己感兴趣低代码的form平台表单实现原理,自己查了些资料,写了一个能跑通的demo,但是原理是没有问题的,可能里面还是会有bug,欢迎大家评论区提出,周末还在写文章,看在辛苦的份上,大哥点个赞吧,😀

下面的代码使用ramda库重构了一版,自己跑了一下,暂时没发现问题。本文后续计划如下:

  • 加入typescript
  • 加入jest测试函数功能
  • 加入可视化的表单生成界面

完整代码 ramda版本

import ReactDOM from 'react-dom';
import React, { useState, useContext, useEffect, useRef, cloneElement } from 'react';
import { path, clone, assocPath, type, equals, pipe, __, all, when, ifElse, F, forEach, reduce } from 'ramda';
import { Input } from 'antd';

// 常量模块
const ALL = Symbol('*');
const FORM_SIGN = Symbol('formStoreSign');
const INNER_HOOKS_SIGN = Symbol('innerHooks');

// 工具函数模块
function isString(name) {
  return type(name) === 'String';
}

function isArray(name) {
  return type(name) === 'Array';
}

function isArrayAndNotNilArray(name) {
  if(type(name) !== 'Array') return false;
  return name.length === 0 ? false : true;
}

function isUndefined(name) {
  return type(name) === 'Undefined';
}

function isObject(name) {
  return type(name) === 'Object';
}

function strToArray(name) {
  if (isString(name)) return [name];
  if (isArray(name)) return name;
  throw new Error(`${name} params need to an Array or String`);
}

function isStrOrArray(name) {
  return isString(name) || isArray(name);
}

const returnNameOrTrue = returnName => name => {
  return returnName ? name : true;
}

function isNameType(name, returnName = false) {
  return ifElse(
    isStrOrArray,
    returnNameOrTrue(returnName),
    F,
  )(name)
}

function checkSchema(schema) {
  ifElse(
    isArrayAndNotNilArray,
    forEach(checkFormItems),
    () => { throw new Error('formItems property of schema need to an Array') }
  )(path(['formItems'], schema));
}

function checkFormItems(item) {
  if (!all(equals(true))([isObject(item), isNameType(item.name)])) {
    throw new Error('please check whether formItems field of schema meet the specifications');
  }
}

function setFormReduce(acc, item) {
  if (!isUndefined(item.value)) {
    acc = assocPath(strToArray(item.name), item.value, acc)
  }
  return acc;
}

function setSchemaToValues(initialSchema) {
  return pipe(
    path(['formItems']),
    reduce(setFormReduce, {})
  )(initialSchema)
}

const warningFunc = () => {
  console.warn(
    'Please make sure to call the getInternalHooks correctly'
  );
};

export const FormContext = React.createContext({
  getInnerHooks: () => {
    return {
      getFieldsValue: warningFunc,
      getFieldValue: warningFunc,
      setFieldValue: warningFunc,
      setFieldsValue: warningFunc,
      isSamePath: warningFunc,
      subscribe: warningFunc,
      notify: warningFunc
    };
  }
});


function pickPaths(root, collects = [], resultPaths = []) {
  function dfs(root, collects) {
    if (isObject(root)) {
      return dfsObj(root)
    }
    if (isArray(root)) {
      return dfsArr(root)
    }
    return resultPaths.push({ path: collects, value: root })
  }

  function dfsObj(root) {
    Object.keys(root).map((item) => {
      const newCollect = clone(collects)
      newCollect.push(item)
      return dfs(root[item], newCollect)
    })
  }
  function dfsArr(root) {
    root.map((item, index) => {
      const newCollect = clone(collects)
      newCollect.push(index)
      return dfs(item, newCollect)
    })
  }
  dfs(root, collects)
  return resultPaths
}

class FormStore {
  constructor(initialValue) {
    this.initialValue = initialValue
    this.values = initialValue ? clone(initialValue) : {}
    this.listeners = []
  }
  getFieldsValue = () => {
    return clone(this.values)
  }

  getFieldValue = (name) => {
    return ifElse(
      isNameType,
      pipe(strToArray, path(__, this.values)),
      F,
    )(name, true)
  }
  setFieldValue = (name, value) => {
    pipe(
      strToArray,
      (newName) => {
        this.values = assocPath(newName, value, this.values);
        this.notify(name);
      },
    )(name)
  }

  setFieldsValue = (value) => {
    return when(
      isObject,
      pipe(pickPaths, forEach((item) => {
        this.values = assocPath(item.path, item.value, this.values)
      }), () => this.notify(ALL)),
    )(value)
  }

  notify = (name) => {
    for (const listener of this.listeners) listener(name)
  }


  subscribe = (listener) => {
    this.listeners.push(listener)
    return () => {
      const index = this.listeners.indexOf(listener)
      if (index > -1) this.listeners.splice(index, 1)
    }
  }


  getFormExport = (schema) => {
    return {
      signType: FORM_SIGN,
      getFieldValue: this.getFieldValue,
      setFieldValue: this.setFieldValue,
      setFieldsValue: this.setFieldsValue,
      isSamePath: this.isSamePath,
      getFieldsValue: this.getFieldsValue,
      getInnerHooks: this.getInnerHooks(schema)
    }
  }


  isSamePath = (path1, path2) => {
    if (type(path1) !== 'Array' || type(path2) !== 'Array') {
      throw new Error('isSamePath函数的参数均需数组')
    }
    return equals(path1, path2)
  }


  getInnerHooks = schema => sign => {
    if (sign === INNER_HOOKS_SIGN) {
      return {
        getFieldsValue: this.getFieldsValue,
        getFieldValue: this.getFieldValue,
        setFieldValue: this.setFieldValue,
        setFieldsValue: this.setFieldsValue,
        isSamePath: this.isSamePath,
        subscribe: this.subscribe,
        notify: this.notify,
        schema
      }
    }
    console.warn('外部禁止使用getInnerHooks方法');
    return null;
  }
}

const useForm = (props) => {
  checkSchema(props.schema);
  const schemaRef = useRef(props.schema);
  const formRef = useRef();
  if (!formRef.current) {
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  if (JSON.stringify(props.schema) !== JSON.stringify(schemaRef.current)) {
    schemaRef.current = props.schema;
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  return [formRef.current];
};

function FormItem(props) {
  const { name, children } = props;

  // 这个是获得store的Context,后面会有讲解
  const innerForm = useContext(FormContext);

  // 如果我们new FormStore有
  const [value, setValue] = useState(name && innerForm ? innerForm.getFieldValue(name) : undefined);

  useEffect(() => {
    if (!name || !innerForm) return;
    return innerForm.subscribe((n) => {
      if (n === ALL || (Array.isArray(n)
        && innerForm.isSamePath(n, strToArray(name)))) {
        setValue(innerForm.getFieldValue(name));
      }
    });
  }, [name, innerForm, innerForm]);

  return cloneElement(children, {
    value,
    onChange: (e) => {
      innerForm.setFieldValue(name, e.target.value);
    }
  });
}

const WigetsMap = {
  input: Input
}

function Form(props) {
  if (props.form.signType !== FORM_SIGN) throw new Error('form类型错误');

  const { form, ...restProps } = props;
  const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
  return (
    <form
      {...restProps}
      onSubmit={(event) => {
        event.preventDefault();
        event.stopPropagation();


      }}
    >
      <FormContext.Provider value={innerForm}>
        {innerForm.schema.formItems.map((item, index) => {
          return (
            <FormItem key={index}
              name={item.name}
              {...item.options}
            >
              {WigetsMap[item.Wiget] ? <item.Wiget {...item.WigetOptions} /> : null}
            </FormItem>
          );
        })}
      </FormContext.Provider>
    </form >
  );
}

const schema = {
  formItems: [
    {
      name: 'account',
      value: 1,
      options: {
      },
      Wiget: 'input'
    }
  ]
}

const Demo = () => {
  const [form] = useForm({ schema });
  window.f = form;
  return <Form form={form} />;
};

ReactDOM.render(
  <Demo />,
  document.getElementById('app')
);


转载自:https://juejin.cn/post/6989060867983671327
评论
请登录