20211015Form


Form

一、已完成功能

  • 验证
  • 提交
  • 重置
  • 设置属性

二、目录结构

image-20211015153328739

三、代码解读

// form.tsx部分代码

const InternalForm = React.forwardRef<unknown, FormProps>((props, ref) => {
  // 使用name组成的对象
  let store:any = {}
  
  const { 
    name,
    layout='horizontal', 
    children, 
    onFinish, 
    labelCol,
    wrapperCol,
    component:Component='form',
    initialValues
  } = props;
    
  const [prefixCls, setPrefixCls] = useGetPrefixCls();
  const [refs, setRefs] = useRefs<any>();
  const [model, setModel] = useSetState(store)
...... 
<FormContext.Provider value={{
      prefixCls,
      labelCol,
      wrapperCol,
      model,
      setModel,
      initialValues
    }}>
      <Component 
        id={name} 
        className={classes}
        onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
          event.preventDefault();
          event.stopPropagation();
          onSubmit()
        }}
        >{
          React.Children.map(children,(child, index)=>{
            // 具有name属性的字段保存下来,方便验证
            const { name, valuePropName} = (child as any)?.props
            name && (store[name] = initialValues?.[name]?initialValues[name]:valuePropName==='checked'?false:'')
            if (!React.isValidElement(child)) return null;
            return React.cloneElement(child,{
              ref:setRefs(index)
            })
          })
        }</Component>
    </FormContext.Provider>

form的主要操作是获取子组件的方法(通过传入的ref),例如验证和重置,因为有三层的嵌套,传参使用context的方式

// form-item-input.tsx代码
import * as React from 'react'
import AsyncValidator from 'async-validator';
import  c from 'classnames'
import { Col } from '../index'
import { ColProps } from '../grid/col';
import { FormContext } from './context'
import { isObject } from '@xt-ui/utils';

export interface FormItemInputRef {
  validate:Function
}
export interface FormItemInputProps {
  colon?: boolean;
  valuePropName?:string;
  label?: React.ReactNode;
  wrapperCol?: ColProps;
  rules?: any;
  [propName: string]: any;
}

const FormItemInput = React.forwardRef<unknown, FormItemInputProps>((props, ref) => {
  const { children, wrapperCol, rules, valuePropName, name } = props
  const formContext = React.useContext(FormContext)
  let { 
    model, 
    setModel,
    prefixCls, 
    initialValues
   } = formContext
  
  const [error, setError] = React.useState('')
  
  // 传给父级验证方法
  React.useImperativeHandle(ref,()=>{
    return {
        props,
        validate,
        resetField
    }
  })

  // !如果自己设置了col着覆盖父级的col
  const mergedWrapperCol: ColProps = wrapperCol || formContext.wrapperCol || {};

  // 验证
  const validate = (trigger: string, cb?: Function): boolean | void => {
    // 获取对应rules
    const rules = getFilteredRule(trigger);

    if (!rules || rules.length === 0) {
      if (cb instanceof Function) {
        cb();
      }

      return true;
    }
    const descriptor = {[name]: rules };
    // 存放rule以字典形式
    const validator = new AsyncValidator(descriptor);
    const model = { [name]: fieldValue() };
    validator.validate(model, { firstFields: true }, (errors) => {
      setError(errors ? errors[0].message! : '')

      if (cb instanceof Function) {
        cb(errors);
      }
    });
  }

  const resetField = () => {
    // 清除验证提示
    setError('')
    if(Object.keys(model).length>0){
      // !重置如果有initialValues则显示initialValues,否则置空,input的value不能为null或者undefined
      model[name] = initialValues?.[name]?initialValues[name]:valuePropName==='checked'?false:''
      setModel(model)
    }
  }

  // 获取rules下的value
  const fieldValue = () => {
    if (!model || !name) { return; }
    return rules && model[name]
  }

  /**
   * @description: 查找对应的rules
   * @param {string} trigger 事件类型
   * @return {*}
   * @author: wxy
   */
  const getFilteredRule = (trigger: string): Array<any> => {
    const rulesList = [].concat(rules);

    return rulesList.filter((rule:any) => {
      if (!rule?.trigger || trigger === '') return true;
      if (Array.isArray(rule.trigger)) {
        return rule.trigger.indexOf(trigger) > -1;
      } else {
        return rule.trigger === trigger;
      }
    }).map((rule:any) => Object.assign({}, rule));
  }

  /**
   * @description: 通过绑定事件获取实时的值
   * @param {React} e 事件对象或者选中状态
   * @author: wxy
   */  
  const getControlField = (e:React.ChangeEvent<any>) => {
    let value = e?.target?.value
    // console.log(isObject(e));
    
    if(name){
      // !onChange返回的值不一样,需要判断
      const fieldValue = isObject(e) ? value : e
      model[name] = fieldValue
      validate('change');
      setModel(model)

      // 给子组件添加onChange事件会和本身的onChange起冲突
      React.Children.map(children,(child:any) => {
          child.props?.onChange?.(fieldValue)
      })
    }
  }

  return (
   <Col {...mergedWrapperCol} className={c([`${prefixCls}-item-control`],{
    [`${prefixCls}-item-has-error`]: error !== ''
   })}>
    <div
      className={c([`${prefixCls}-item-control-content`])}>
      {
      /* 为表单元素添加交互事件 */
        React.Children.map(children,child => {
          return React.cloneElement(child as any,{
            onChange:getControlField,
            value:model[name]
          })
        })
      }
    </div>
    {error && <div className={c([`${prefixCls}-item-error`])}>{error}</div>}
  </Col>
  )
})

export default FormItemInput

这里会给每一个表单组件都绑定一个onChange和value(核心概念),或者对应的属性就行操作,有一个model属性,通过父组件传过来,onfinsh也是返回这个model,这个model不能自己定义,要不然会有bug,需要使用useState定义

四、验证功能

验证功能使用的是async-validator插件

import AsyncValidator from 'async-validator';

// 验证
const validate = (trigger: string, cb?: Function): boolean | void => {
  // 获取对应rules
  const rules = getFilteredRule(trigger);

  if (!rules || rules.length === 0) {
    if (cb instanceof Function) {
      cb();
    }

    return true;
  }
  const descriptor = {[name]: rules };
  // 存放rule以字典形式
  const validator = new AsyncValidator(descriptor);
  const model = { [name]: fieldValue() };
  validator.validate(model, { firstFields: true }, (errors) => {
    setError(errors ? errors[0].message! : '')

    if (cb instanceof Function) {
      cb(errors);
    }
  });
}

Author: wxy
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source wxy !
  TOC