在工作中往往都是使用第三方UI库来做表单功能,这次在使用 vue3+tsx 重写记账项目的过程中打算通过原生form标签来实现表单校验功能。
前期准备
- 一个🌰:校验 name 和 remark 这两个字段,失败显示对应的校验失败原因
<form>
<div>
<label>
<span>name</span>
<input/>
</label>
</div>
<div>
<label>
<span>remark</span>
<input/>
</label>
</div>
<div>
<button>确定</button>
</div>
</form>
- 确定校验规则
- name 为必填
- remark 为必填并且最多只能输入7个字符
- 确定触发表单提交校验的方式 通过监听表单的onSubmit事件
为什么不监听按钮的点击事件???
不一定通过点击按钮提交 ,提交也可通过回车、键盘事件触发 所以不要只监听鼠标的点击事件来提交。
- 校验和form组件分离 有些UI库是将校验写在了form组件中,不打算使用这种方案,希望各自做各自的活。
具体实现
- 定义关系
const errors= validate(formData, rules)
向 validate 函数传入表单数据以及校验规则,返回错误信息。
- 定义结构
- rules是数组而不是对象,因为需要具有先后校验的顺序,数组里面具体描述校验的每一项规则并且定义错误提示。
- errors是对象结构,key为校验的字段,展示错误信息
const formData = reactive({
name: "",
sign: "",
});
const rules= [
{ key: "name",
type: "required",
message: "必填"
},
{
key: "remark",
type: "pattern",
regex: /^.{1,7}$/,
message: "只能填 1 到 7 个字符",
},
];
const errors = reactive({
name: ['错误1','错误2'],
mark: ['错误1']
});
- validate 逻辑
将传入的rules遍历通过校验类型来做逻辑判断校验是否失败,失败则将message以对象形式放入errors中,将errors暴露给外部使用。
const errors = {}
rules.map(rule => {
const { key, type, message } = rule
const value = formData[key]
switch (type) {
case 'required':
if (value === null || value === undefined || value === '') {
errors[key] = errors[key] ?? []
errors[key]?.push(message)
}
break;
case 'pattern':
if (value && !rule.regex.test(value.toString())) {
errors[key] = errors[key] ?? []
errors[key]?.push(message)
}
break;
default:
return
}
})
return errors
- 在提交方法中使用它
const errors = {}
// 清空上一次的错误记录
Object.assign(errors, {
name: undefined,
remark: undefined
})
// 获取到新的校验失败内容
Object.assign(errors, validate(formData, rules))
// 表单事件onsubmit默认会刷新页面,阻止页面刷新
e.preventDefault()
typescript 小课堂
- type 不可直接循环引用自身 🥲, interface 可以做到!
定义 formData 类型
// error
type XData = Record<string, string | number | null | undefined | XData>;
// success
interface FData {
[k: string]: string | number | null | undefined | FData
}
- rules 只有 required 以及 pattern 两种规则,ts使用|实现2选1,测试居然两种规则可并存???
type XRule = {
key: string;
message: string;
} & ({ required: boolean } | { RegExp: RegExp });
// r 竟然可以同时拥有required和RegExp???
const r: XRule = {
key: "name",
message: "必填",
required: true,
RegExp: /^.{1,7}$/,
};
A | B 要实现2选1功能,A与B就必须有**互斥属性 **
type FRule = {
key: string;
message: string;
} & ({ type: "required" } | { type: "pattern"; regex: RegExp });
// 将报错
const r2: FRule = {
key: "name",
message: "必填",
required: true,
RegExp: /^.{1,7}$/,
};
- 应用范型 Rules的key和errors的key都只能是FData中的key,如何将他们关联? 使用范型
让T属于FData
interface FData {
[k: string]: string | number | null | undefined | FData
}
type Rule<T> = {
key: keyof T
message: string
} & (
{ type: 'required' } |
{ type: 'pattern', regex: RegExp }
)
type Rules<T> = Rule<T>[]
export type { Rules, Rule, FData }
export const validate = <T extends FData>(formData: T, rules: Rules<T>) => {
type Errors = {
[k in keyof T]?: string[]
}
const errors: Errors = {}
}
最终代码
validate.tsx
interface FData {
[k: string]: string | number | null | undefined | FData
}
type Rule<T> = {
key: keyof T
message: string
} & (
{ type: 'required' } |
{ type: 'pattern', regex: RegExp }
)
type Rules<T> = Rule<T>[]
export type { Rules, Rule, FData }
export const validate = <T extends FData>(formData: T, rules: Rules<T>) => {
type Errors = {
[k in keyof T]?: string[]
}
const errors: Errors = {}
rules.map(rule => {
const { key, type, message } = rule
const value = formData[key]
switch (type) {
case 'required':
if (value === null || value === undefined || value === '') {
errors[key] = errors[key] ?? []
errors[key]?.push(message)
}
break;
case 'pattern':
if (value && !rule.regex.test(value.toString())) {
errors[key] = errors[key] ?? []
errors[key]?.push(message)
}
break;
default:
return
}
})
return errors
}
Form.tsx
import { defineComponent, PropType, reactive } from 'vue';
import { Rules, validate } from '../../shared/validate';
export const Form = defineComponent({
setup: (props, context) => {
const formData = reactive({
name: '',
remark: '',
})
const errors = reactive<{ [k in keyof typeof formData]?: string[] }>({})
const onSubmit = (e: Event) => {
const rules: Rules<typeof formData> = [
{ key: 'name', type: 'required', message: '必填' },
{ key: 'name', type: 'pattern', regex: /^.{1,7}$/, message: '只能填 1 到 7个字符' },
{ key: 'remark', type: 'required', message: '必填' },
]
// 清空上一次的错误记录
Object.assign(errors, {
name: undefined,
remark: undefined
})
Object.assign(errors, validate(formData, rules))
// 表单事件onsubmit默认会刷新页面,阻止页面刷新
e.preventDefault()
}
return () => (
<form onSubmit={onSubmit}>
<div>
<label>
<span>name</span>
<input v-model={formData.name} />
<span>{errors['name'] ? errors['name'][0] : ' '}</span>
</label>
</div>
<div>
<label>
<span>renark</span>
<input v-model={formData.remark} />
<span>{errors['remark'] ? errors['remark'][0] : ' '}</span>
</label>
</div>
<button>确定</button>
</form>
)
}
})