一篇文章說清楚低代碼form表單平臺 -- form內核原理

前言

咱們的目標是將schema => form表單,schema就是一個json對象,咱們看看阿里的form-render庫(一個低代碼reactform表單庫)的schema長什麼樣子:javascript

{
  "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好像也是相似的用法:vue

<Form 這裏能夠定義Form的屬性>
    <Form.Item name="account" 這裏能夠定義Form.Item的屬性> <Input 這裏能夠定義表單組件的屬性 /> </Form.Item>
    <Form.Item name="password"> <Input 這裏能夠定義表單組件的屬性 /> </Form.Item>
</Form>
複製代碼

因此對應跟組件使用差很少的schema定義以下:java

{
    // 至關於在上面的 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: '更改這裏' }}react

  • 還能夠定義爲[a, 1],會被解析爲更改{ a: [ undefined, '更改這裏' ] },typescript

經過這個name的命名設置,咱們能夠看到,既能知足數組嵌套,也能知足對象嵌套,因此能夠知足幾乎所有表單對象值的格式要求。json

因此咱們但願form內核大概使用的方式是:redux

// 定義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就能夠改裝啊,可是你要知道,markdown

可是ant自己一些屬性是函數,JSON上是不能掛函數的,由於JSON.stringify會把函數過濾掉,因此,不少ant屬性須要掛函數,內部就不支持了,好比onFinish事件,shouldUpdate方法等等antd

還有若是咱們業務某個產品須要不少自定義的需求,可能涉及到要改底層的form庫,就須要本身開發一套了,因此魔改antform不太好,還不如本身開發一套呢

廢話很少說,開始編碼!

大致架構

咱們的大致架構以下(沒有寫form渲染器器(便可視化拖拽表單這塊功能)後續加):

image.png

上圖比較簡陋,咱們先把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的重要性了。

歡迎你們去個人博客裏看,以一篇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') ); 複製代碼
相關文章
相關標籤/搜索