form表单组件

2019-12-05  本文已影响0人  sweetBoy_9126

设计api

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事件函数,拿到你当前的表单中的数据

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}>
  )
}
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

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

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

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 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

export interface FormValue {
  [k: string]: any
}
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}]

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;
const onSubmit = () => {
    const rules = [
      {key: 'username', required: true}
    ]
    const errors = Validator(formData, rules)
    console.log(errors, 'errors')
  }

展示error

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>
      )}
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

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

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})

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

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>
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)
            })
          }
        }},
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]
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)
  }
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])))
  })
上一篇下一篇

猜你喜欢

热点阅读