设计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> </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])))
})
网友评论