美文网首页
form表单组件

form表单组件

作者: sweetBoy_9126 | 来源:发表于2019-12-05 14:31 被阅读0次

    设计api

    • form.tsx
    import * as React from 'react'
    import {ReactFragment} from "react";
    interface Props {
      value: {[k: string]: any};
      fields: Array<{ name: string, label: string, input: { type: string }}>;
      buttons: ReactFragment;
    }
    const Form:React.FunctionComponent<Props> = (props) => {
      return (
        <form>
          {props.fields.map(f =>
            <div key={f.name}>
              {f.label}
              <input type={f.input.type}/>
            </div>
          )}
          <div>
            {props.buttons}
          </div>
        </form>
      )
    }
    export default Form
    

    使用

    import * as React from 'react'
    import Form from './form'
    import {Fragment, useState} from "react";
    
    const FormExample:React.FunctionComponent = () => {
      const [formData] = useState({
        username: '',
        password: ''
      })
      const [fields] = useState([
        { name: 'username', label: '用户名', input: { type: 'text'} },
        { name: 'password', label: '密码', input: { type: 'password'} },
      ])
      return (
        <div>
          <Form value={formData} fields={fields}
            buttons={
              <Fragment>
                <button type="submit">提交</button>
                <button>返回</button>
              </Fragment>
            }
          ></Form>
        </div>
      )
    }
    export default FormExample
    

    接受一个onSubmit事件,指定传入的buttons里的按钮type为submit,当点击提交时,触发外界传入的submit事件函数,拿到你当前的表单中的数据

    • form.tsx
    interface Props {
      onSubmit: React.FormEventHandler;
    }
    const Form:React.FunctionComponent<Props> = (props) => {
      const onSubmit: React.FormEventHandler = (e) => {
        e.preventDefault()
        props.onSubmit(e)
      }
      return (
        <form onSubmit={onSubmit}>
      )
    }
    
    • form.example.tsx
    const FormExample:React.FunctionComponent = () => {
      const onSubmit = () => {
        console.log(formData)
      }
      return (
        <Form value={formData} fields={fields}
            buttons={
              <Fragment>
                <button type="submit">提交</button>
                <button>返回</button>
              </Fragment>
            }
            onSubmit={onSubmit}
      )
    }
    

    在我们的input里绑定我们的value

    • form.tsx
    const formData = props.value
    <input type={f.input.type} value={formData[f.name]}
    

    这时因为我们的input是受控组件所以必须接受一个onChage来修改它的value才能更新ui,所以我们在form组件里通过onChange拿到当前你输入的值,然后调用父组件的onChange把值传出去

    • from.tsx
    interface FormValue {
      [k: string]: any
    }
    interface Props {
       onChange: (value: FormValue) => void;
    }
     const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        props.onChange(e.target.value)
      }
      return (
        <form onSubmit={onSubmit}>
          {props.fields.map(f =>
            <div key={f.name}>
              {f.label}
              <input type={f.input.type} value={formData[f.name]}
                onChange={onInputChange}
              />
            </div>
          )}
    
    • form.example.tsx
    <Form value={formData} fields={fields}
            buttons={
              <Fragment>
                <button type="submit">提交</button>
                <button>返回</button>
              </Fragment>
            }
            onSubmit={onSubmit}
            onChange={(value) => setFormData(value)}
          />
    

    问题:因为我们传出去的value是我们input里输入的值,但是我们怎么能对应的上它的key哪?我们不知道是username的还是password的,所以我们要把传出去的值变成键值对的形式

    const onInputChange = (name: string, value: string) => {
        const newFormData = {...props.value, [name]: value}
        props.onChange(newFormData)
      }
      <input type={f.input.type} value={formData[f.name]}
                onChange={(e) => onInputChange(f.name, e.target.value)}
              />
    

    然后在使用的时候给setState传入我们的FormValue类型,在form里导出我们的FormValue

    • form.tsx
    export interface FormValue {
      [k: string]: any
    }
    
    • form.example.tsx
    const [formData, setFormData] = useState<FormValue>({
        username: 'lifa',
        password: ''
      })
    

    表单验证

    api设计

    errors = Validator(data, rules)
    errors结构: 对象,里面的key对应着验证的属性,value是数组,数组里面的每一项对应着错误。如:
    { username: ['字符太短', '含有不合法的字符'], password: ['不合法'] }
    rules结构:数组,数组里的每一项是一个对象,对象里有key是需要验证的字段,然后后面是验证规则。
    如: [{key: 'username', maxlength: 8, minlength: 6}]

    • Validator.tsx
    import {FormValue} from "./form";
    
    interface FormRule {
      key: string;
      required?: boolean;
      minLength?: number;
      maxLength?: number;
    }
    
    type FormRules = Array<FormRule>
    
    interface FormErrors {
      [k: string]: string[];
    }
    
    const Validator = (formValue: FormValue, rules: FormRules): FormErrors => {
      const errors: FormValue = {}
      rules.map(rule => {
        const value = formValue[rule.key]
        if (rule.required) {
          if (value === undefined || value === null || value === '') {
            errors[rule.key] = ['必填']
          }
        }
        console.log(rule)
      })
      return errors;
    }
    export default Validator;
    
    • form.example.tsx
    const onSubmit = () => {
        const rules = [
          {key: 'username', required: true}
        ]
        const errors = Validator(formData, rules)
        console.log(errors, 'errors')
      }
    

    展示error

    • form.tsx
    interface Props {
      errors: {[k: string]: string[]}
    }
    
     {props.fields.map(f =>
            <div key={f.name} className={sc('item')}>
                <div className={sc('error')}>
                  {props.errors[f.name]}
                </div>
            </div>
          )}
    
    • form.example.tsx
    const [errors, setErrors] = useState({})
      const onSubmit = () => {
        const rules = [
          {key: 'username', required: true},
          {key: 'username', minLength: 8, maxLength: 16},
          {key: 'username', pattern: /[A-za-z0-9]/}
        ]
        const errors = Validator(formData, rules)
        setErrors(errors)
      }
      return (
        <div>
          <Form value={formData} fields={fields}
            buttons={
              <Fragment>
                <button type="submit">提交</button>
                <button>返回</button>
              </Fragment>
            }
            onSubmit={onSubmit}
            onChange={(value) => setFormData(value)}
            errors={errors}
          />
        </div>
      )
    

    input组件

    import * as React from "react";
    import {InputHTMLAttributes} from "react";
    import {scopedClassMaker} from '../helpers/classes';
    const sc = scopedClassMaker('ireact-input')
    import './input.scss'
    interface Props extends InputHTMLAttributes<HTMLInputElement>{
    
    }
    const Input: React.FunctionComponent<Props> = ({className, ...rest}) => {
      return (
        <input className={sc('', {extra: className})} {...rest}/>
      )
    }
    export default Input;
    

    在form里使用table实现每一行的对齐

    <table>
            {props.fields.map(f =>
              <tr key={f.name} className={sc('row')}>
                <td>
                  <span className={sc('label')}>
                    {f.label}
                  </span>
                </td>
                <td>
                  <Input type={f.input.type} value={formData[f.name]}
                         onChange={(e) => onInputChange(f.name, e.target.value)}
                  />
                  <div>{props.errors[f.name]}</div>
                </td>
              </tr>
            )}
            <div>
              {props.buttons}
            </div>
          </table>
    

    validator自定义

    我们想支持自定义校验,只需要我们传入一个自己的validate方法就行,比如下面的

    const rules = [
      { key: 'username', validator: {
        name: 'unique',
        validate(username: string) {
          axios.get('/check_username', {params: {username}})
          .then().catch()
        }
       }}
    ]
    

    问题我们的validate最后是要返回一个true或者false的,但是我们如果是异步的话就不能直接return了,所以我们需要返回一个promise

    validate(username: string) {
         return axios.get('/check_username', {params: {username}})
          .then().catch()
        }
    

    自己写一个checkUserName

    • example.tsx
    const userName = ['lifa', 'yitong', 'meinv']
      const checkUserName = (username: string, success: () => void, fail: () => void) {
        setTimeout(() => {
          if (userName.indexOf(username) >= 0) {
            success()
          } else {
            fail()
          }
        }, 3000)
      }
    
    validate(username: string) {
        return new Promise((resolve, reject) => {
          checkUserName(username, resolve, reject)
        })
    }
    

    实现validator

    • validator.tsx
    interface Validator {
      name: string,
      validate: (username: string) => Promise<void>
    }
    interface FormRule {
      validator?: Validator
    }
    
    rules.map(rule => {
        const value = formValue[rule.key]
        if (rule.validator) {
          // 自定义的校验器
          const promise = rule.validator.validate(value)
        }
    }
    

    把我们自定义校验器里的Promise也添加到error里

    const addError = (key: string, message: string | Promise<void>) 
    const promise = rule.validator.validate(value)
    addError(rule.key, promise)
    

    问题1:因为我们的校验checkUserName是异步的所以当我们调用validate的时候他拿到的结果是Promise未执行完的,我们需要它异步完成后再执行validate,也就是说不直接拿到errors,而是在全部的promise执行完后再返回errors
    方法:先拿到所有的报错把每一个的数组都合成一个,然后用Promise.all

    console.log(Object.values(errors)) // 是一个数组,里面的每一项又是一个数组,所以我们需要把它们都拿出来放到一个数组里
    function flat(arr: Array<any>): Array<any> {
      const result = []
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] instanceof Array) {
          result.push(...arr[i])
        } else {
          result.push(arr[i])
        }
      }
      return result
    }
    // 让Validator接受一个回调,所有的promise成功或者失败的时候才调用这个回调,所以我们我们通过这个回调把errors传出去
    const Validator = (formValue: FormValue, rules: FormRules, callback: (errors: Array<any>) => void): FormErrors => {
    Promise.all(flat(Object.values(errors)))
        .then(() => {callback(errors), () => callback(errors)})
    }
    

    外面直接传我们需要执行的回调

    const errors = Validator(formData, rules, (errors) => {
          console.log(errors)
     })
    

    这时候我们等到promise都执行完成后会打印出下面的结果

    问题2:我们光拿到个Promise没有用啊,我们需要拿到一个错误状态,比如用户名已存在啥的,那么怎么样对传出去的Promise进行修改那?
    方法:对addError方法进行修改可以同时接受message和promise

    interface OneError {
      message: string;
      promise?: Promise<void>;
    }
    
    const addError = (key: string, error: OneError) : void => {
        if (errors[key] === undefined) {
          errors[key] = []
        }
        errors[key].push(error)
      }
    rules.map(rule => {
        const value = formValue[rule.key]
        if (rule.validator) {
          // 自定义的校验器
          const promise = rule.validator.validate(value)
          addError(rule.key, {message: '用户名已经存在', promise})
        }
        if (rule.required && !isEmpty(value)) {
          addError(rule.key, {message: '必填'})
        }
    }
    // 拿到一个只包含promise的数组
    const promiseList = flat(Object.values(errors))
        .filter(item => item.promise)
        .map(item => item.promise)
      Promise.all(promiseList).then(() => console.log(errors))
    

    promise全部执行完成我们需要把erros进行处理,把里面的promise字段去掉,只把message给出去

    function fromEntries(array: Array<[string, string[]]>): object {
      const result: {[key: string]: string[]} = {}
      for (let i = 0; i < array.length; i++) {
        result[array[i][0]] = array[i][1]
      }
      return result
    }
    const promiseList = flat(Object.values(errors))
        .filter(item => item.promise)
        .map(item => item.promise)
      Promise.all(promiseList)
        .then(() => {
          const newErrors = fromEntries(Object.keys(errors).map(key =>
            [key, errors[key].map((item: OneError) => item.message)]
          ))
          callback(newErrors)
        }, () => {
          const newErrors = fromEntries(Object.keys(errors).map(key =>
            [key, errors[key].map((item: OneError) => item.message)]
          ))
          callback(newErrors)
        })
    

    使用

     Validator(formData, rules, (errors) => {
          setErrors(errors)
        })
    

    问题3:我们上面的自定义message是写死的“用户已存在”,但实际上他有可能是别的?
    解决办法:将message改成它的key

    const promise = rule.validator.validate(value)
    addError(rule.key, {message: rule.validator.name, promise})
    

    然后用户可以指定错误类型,我们提供了默认的错误类型

    • form.tsx
    interface Props {
      transformError?: (error: string) => string;
    }
    const transformError = (error: string): string => {
        const map: {[key: string]: string} = {
          required: '必填',
          minLength: '字符长度过短',
          maxLength: '字符长度过长',
          pattern: '格式不正确'
        }
        console.log(error, 'error')
        return props.transformError!(error) || map[error]
      }
    <div className={sc('error')}>
                    {
                      props.errors[f.name] ?
                      transformError!(props.errors[f.name][0]) :
                      <span>&nbsp;</span>
                    }
                  </div>
    
    • from.example.tsx
    const tranformError = (message: string) => {
        const map: {[key: string]: string} = {
          unique: '用户名已存在'
        }
        return map[message]
      }
      return (
        <div>
          <Form value={formData} fields={fields}
            buttons={
              <Fragment>
                <Button defaultType="submit" type="primary">提交</Button>
                <Button>返回</Button>
              </Fragment>
            }
            onSubmit={onSubmit}
            transformError={tranformError}
            onChange={(value) => setFormData(value)}
            errors={errors}
          />
        </div>
      )
    

    超难异步bug解决过程

    问题1:当我们对用户名校验是否存在的时候,不管存不存在它都会提示存在
    问题2:如果我们有多个异步校验的话我们的Promise.all会在第一个异步结束后就执行完,这就导致我们后面的异步校验没法正常执行,比如:

    {key: 'username', validator: {
            name: 'unique',
              validate(username: string) {
                console.log('有人调用了validate')
                return new Promise<void>((resolve, reject) => {
                  checkUserName(username, resolve, reject)
                })
              }
            }},
          {key: 'username', validator: {
              name: 'unique2',
              validate(username: string) {
                console.log('有人调用了validate')
                return new Promise<void>((resolve, reject) => {
                  checkUserName(username, resolve, reject)
                })
              }
            }},
    
    • validator.tsx
    Promise.all(flat(Object.values(errors))
        .filter(item => item.promise)
        .map(item => item.promise)
      ).finally(() => {
        console.log('all 运行完了')
        callback(
          fromEntries(
            Object.keys(errors)
              .map(key => [key, errors[key].map((item: OneError) => item.message)])
          )
        )
      })
    

    改进:我们需要把一个全是promise的对象改成全是string的,比如:
    转换前的结构

    {
      username:  [Promise1, Promise2],
      password: [Promise3, Promise4]
    }
    

    转换后的结构

    {
      username: ['error1', 'error2'],
      password: ['error3', 'error4']
    }
    

    而我们只能使用Promise.all(array)这个api

    第一层转换

    {
      username:  [Promise1, Promise2],
      password: [Promise3, Promise4]
    }
    

    转换成

    ['username', Promise1], ['username', Promise2]
    ['password', Promise3], ['password', Promise4]
    
    • from.example.tsx
    const validator = (username: string) => {
        return new Promise<string>((resolve, reject) => {
          checkUserName(username, resolve, () => reject('unique'))
        })
      }
      const onSubmit = () => {
        const rules = [
         {key: 'username', validator},
          {key: 'username', validator},
          {key: 'password', required: true},
          {key: 'password', required: true}
        ]
        Validator(formData, rules, (errors) => {
          setErrors(errors)
        })
        // setErrors(errors)
      }
    
    • validator.tsx
    interface FormRule {
      key: string;
      required?: boolean;
      minLength?: number;
      maxLength?: number;
      pattern?: RegExp;
      validator?: (value: string) => Promise<string>
    }
    type OneError = string | Promise<string>
    
    
    const Validator = (formValue: FormValue, rules: FormRules, callback: (errors: FormValue) => void): void => {
      const errors: FormValue = {}
      const addError = (key: string, error: OneError) : void => {
        if (errors[key] === undefined) {
          errors[key] = []
        }
        errors[key].push(error)
      }
      rules.map(rule => {
        const value = formValue[rule.key]
        if (rule.validator) {
          // 自定义的校验器
          const promise = rule.validator(value)
          addError(rule.key, promise)
        }
        if (rule.required && !isEmpty(value)) {
          addError(rule.key, 'required')
        }
        if (rule.minLength && isEmpty(value) && value.length < rule.minLength) {
          addError(rule.key, 'minLength')
        }
        if (rule.maxLength && isEmpty(value) && value.length > rule.maxLength) {
          addError(rule.key, 'maxLength')
        }
        if (rule.pattern && !(rule.pattern.test(value))) {
          addError(rule.key,  'pattern')
        }
      })
      console.log(errors)
    }
    
    

    转换:

    const x = Object.keys(errors).map(key =>
        errors[key].map((promise: OneError) => [key, promise])
      )
      console.log(x)
    
    转换2

    将上面的转换成

    ['username', promise1], ['u',p2], ['password', p3], ['p', p4]
    
    const y = flat(x)
    console.log(y)
    
    转换3

    将上面拍平的数组每一个都转成一个新的Promise,比如['username', promise1]转换成Promise<['username', promise1]>

    const z = y.map(([key, promiseOrString]) => promiseOrString.then(() => {
        return [key, undefined]
      }, (reason: string) => {
        return [key, reason]
      }))
      Promise.all(z).then(results => {
        console.log(results)
      })
    
    转换3

    将['username', 'unique]的结构转换成usrname:'unique'

    function zip(kvList: Array<[string, string]>) {
      const result = {}
      kvList.map(([key, value]) => {
        if (!result[key]) {
          result[key] = []
        }
        result[key].push(value)
      })
      return result
    }
    const z = y.map(([key, promiseOrString]) => (
        promiseOrString instanceof Promise ? promiseOrString : Promise.reject(promiseOrString))
        .then(() => {
          return [key, undefined]
        }, (reason: string) => {
          return [key, reason]
        }))
      Promise.all(z).then((results: Array<[string, string]>) => {
        callback(zip(results.filter(item => item[1])))
      })
    

    相关文章

      网友评论

          本文标题:form表单组件

          本文链接:https://www.haomeiwen.com/subject/fyvdgctx.html