表单校验总结
本文将梳理一遍表单的各种校验方法。先从原生校验进行总结,再延伸到 vue 和 Rect 的表单校验。
form 表单原生校验
在 HTML5 中,表单校验有原生的支持,这些校验功能可以在不借助 JavaScript 的情况下进行基本的验证。
必填字段 (required),使用
required
属性可以指定某个字段是必填的。<form> <label for="username">Username:</label> <input type="text" id="username" name="username" required /> <input type="submit" value="Submit" /> </form>
输入类型 (type),HTML5 提供了多种输入类型,每种类型都有其特定的验证规则。
电子邮件 (email):
<label for="email">Email:</label> <input type="email" id="email" name="email" required />
网址 (url):
<label for="website">Website:</label> <input type="url" id="website" name="website" required />
数字 (number):
<label for="age">Age:</label> <input type="number" id="age" name="age" min="18" max="65" step="1" required />
日期 (date),此外还有周(week)、月份(mouth)、时间(time)和日期加时间(datetime)和本地时间(datetime-local)等:
<label for="birthDate">BirthDate:</label> <input type="date" id="birthDate" name="birthDate" min="1900-01-01" max="2024-12-31" required />
输入字段 (pattern),使用
pattern
属性可以指定某个字段的正则表达式。<label for="phone">Phone:</label> <input type="tel" id="phone" name="phone" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" required /> <small>Format: 123-456-7890</small>
输入字段的长度范围 (minlength, maxlength),可以指定文本字段的最小和最大输入长度。
<label for="username">Username:</label> <input type="text" id="username" name="username" minlength="5" maxlength="15" required />
输入字段的数据范围 (min, max),用于
number
和date
类型字段,指定允许的数值或日期范围。<label for="quantity">Quantity (between 1 and 5):</label> <input type="number" id="quantity" name="quantity" min="1" max="5" /> <label for="birthday">Birthday:</label> <input type="date" id="birthday" name="birthday" min="2000-01-01" max="2020-12-31" />
自定义错误消息(setCustomValidity),尽管 HTML5 原生校验会自动生成错误消息,但也可以使用
setCustomValidity
方法来自定义错误消息。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>自定义 Validation</title> </head> <body> <form id="myForm"> <label for="username">Username:</label> <input type="text" id="username" name="username" required /> <button type="submit">Submit</button> </form> <script> // 监听用户名输入事件 document .getElementById('username') .addEventListener('input', function () { // 检查输入的用户名是否至少为5个字符 if (this.value.length < 5) { this.setCustomValidity( 'Username must be at least 5 characters long.' ) } else { this.setCustomValidity('') // 清除自定义错误消息 } }) // 监听表单提交事件 document .getElementById('myForm') .addEventListener('submit', function (event) { const usernameInput = document.getElementById('username') // 使用 checkValidity 方法检查用户名字段的有效性 if (!usernameInput.checkValidity()) { event.preventDefault() // 阻止表单提交,并显示自定义的错误消息。 } }) </script> </body> </html>
这里对原生校验进行总结:form 表单的 checkValidity
函数是 HTML5 表单验证 API 的一部分。它会自动检查表单元素上的所有验证约束,并返回一个布尔值:如果所有约束都通过,返回 true
;否则返回 false
。表单元素的各项验证条件,包括:
- 填字段 (required 属性)
- 类型 (type 属性,例如 email, number 等)
- 模式 (pattern 属性,使用正则表达式验证)
- 最小长度和最大长度 (minlength 和 maxlength 属性)
- 范围 (min 和 max 属性)
- 步长 (step 属性)
- 自定义的验证消息(通过 setCustomValidity 设置),错误消息不为空,则校验不通过。
有了上面的基础,再来看看组件库是如何进行校验的。
element 的 form 校验
Element 提供了 el-form 组件作为表单的容器,里面可以放置各种表单控件,用 model 绑定 formData 数据对象。
每个表单控件可以通过 rules
属性来设置校验规则,这些规则可以是预定义的规则(如必填、长度等)或者自定义规则。Form-Item 中的 prop 属性用于指定 formData 数据对象中的属性名称,Form-Item 中的 label 属性用于指定表单控件的标签。
值得注意的是,Form 中的 rules
属性是一个对象,里面包含多个校验规则,每个校验规则都对应一个 Form-Item。而 Form-Item 的 rules
属性是一个数组,里面包含多个校验规则,每个校验规则都对应一个 Form-Item。它可以覆盖掉 Form 中的校验规则。
当然,Element 也提供了和原生 form 表单一样的常见的校验规则,例如 required
(必填)、email
(邮箱格式)、url
(URL 格式)、number
(数字)、integer
(整数)等。
自定义校验规则(validator):通过绑定一个 validator 规则属性,来自定义校验方法。element 采用的校验方法是 async-validator 库进行校验。validator 校验函数接收三个参数:rule
(当前规则对象)、value
(当前字段的值)、callback
(回调函数,用于返回校验结果)。
触发校验: 当用户提交表单或者手动调用 validate 方法时,Element 都会根据设置的规则对表单进行校验。
来看官网给的一个例子(已简化):
<template>
<el-form
:model="ruleForm"
status-icon
:rules="rules"
ref="ruleForm"
label-width="100px"
>
<el-form-item label="年龄" prop="age">
<el-input v-model.number="ruleForm.age"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
const checkAge = (rule, value, callback) => {
if (!value) {
return callback(new Error('年龄不能为空'))
}
setTimeout(() => {
if (!Number.isInteger(value)) {
callback(new Error('请输入数字值'))
} else {
if (value < 18) {
callback(new Error('必须年满18岁'))
} else {
callback()
}
}
}, 500)
}
return {
ruleForm: {
age: '',
},
rules: {
age: [{ validator: checkAge, trigger: 'blur' }],
},
}
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
alert('submit!')
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
},
}
</script>
在表格中进行校验
在表格中进行校验,在 element 中常用的方法是在 form 表单中包裹一个 table 组件,然后将 form 表单的校验规则绑定到 table 组件的某一行上。但是 table 的行很多,为了对应到指定的行,所以需要对 formItem 的 prop
属性进行一些处理。
上文中提到过,prop
属性是绑定 formData 数据对象中的某个属性,所以需要将 prop
属性绑定到 formData 数据对象中的某个属性,然后通过该属性来对应到表格中的某一行。常用的方式是 :prop="scope.$index+'.name'"
。利用 scope.$index
获取当前行的索引,然后将 prop
属性绑定到 formData
数据对象相应行中的某个属性,最后的结果是绑定 formData[$index].name
。
这里的
:prop="scope.$index+'.name'"
能正确解析为formData[$index].name
的关键在于 Element 内部的实现。它使用了类似lodash.get
的方式来解析路径字符串。function get(object, path) { const keys = path.split('.') // 把 "0.name" 拆解为 ["0", "name"] return keys.reduce((acc, key) => acc[key], object) // 动态访问对象属性 } // 示例: const formData = [{ name: 'Alice' }, { name: 'Bob' }] const value = get(formData, '0.name') // => "Alice"
<template>
<div>
<!-- 绑定 form 便于重置 -->
<el-form :model="tableData" ref="tableForm">
<!-- 内部 table -->
<el-table :data="tableData" border style="width: 100%">
<el-table-column label="年龄">
<template slot-scope="scope">
<el-form-item
:prop="`${scope.$index}.age`"
:rules="tableRules.selfAge"
>
<el-input v-model.number="scope.row.age" />
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
<div>
<el-button type="primary" @click="submitForm('tableForm')">
提交
</el-button>
<el-button @click="resetForm('tableForm')">重置</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
const checkAge = (rule, value, callback) => {
if (!value) {
return callback(new Error('年龄不能为空'))
}
setTimeout(() => {
if (!Number.isInteger(value)) {
callback(new Error('请输入数字值'))
} else {
if (value < 18) {
callback(new Error('必须年满18岁'))
} else {
callback()
}
}
}, 500)
}
return {
tableData: [{ age: '' }],
tableRules: {
selfAge: [{ validator: checkAge, trigger: 'blur' }],
},
}
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
alert('submit!', this.tableData)
} else {
console.error('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
},
}
</script>
antDesign 的 form 校验
antDesign 中的 form 组件,同 element 非常像。其中 rules 的自定义校验数组定义可参考官网,本质上还是更贴近 HTML5 的原生写法。
import { Form, Input, Button } from 'antd'
const MyForm = () => {
const [form] = Form.useForm()
const onFinish = (values) => {
console.log('Success:', values)
}
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
}
return (
<Form
form={form}
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
)
}
export default MyForm
在表格中进行校验,原理同 element 一样,添加 form 表单,但实际上手会更复杂写,看一个实际例子:点击表单可编辑,失焦后恢复。
import React, { useState } from 'react'
import { Table, Input, Form, Button, Popconfirm } from 'antd'
/** 单元格组件 */
const EditableCell = ({
title, // 列标题
editable, // 是否可编辑
children, // 单元格子节点
dataIndex, // 数据索引
record, // 当前行的数据
handleSave, // 保存数据的处理函数
...restProps // 其他属性
}) => {
const [editing, setEditing] = useState(false) // 控制是否处于编辑状态
const [form] = Form.useForm()
const toggleEdit = () => {
setEditing(!editing) // 改变编辑状态
form.setFieldsValue({
// 更新 form 表单, 设置表单字段的值
[dataIndex]: record[dataIndex],
})
}
const save = async () => {
try {
const values = await form.validateFields() // 验证表单字段
toggleEdit() // 切换回非编辑状态
handleSave({ ...record, ...values }) // 调用保存函数,将新的数据传递出去
} catch (errInfo) {
console.error('Save failed:', errInfo) // 捕获并处理保存失败的错误
}
}
let childNode = children
if (editable) {
childNode = editing ? (
<Form form={form} component={false}>
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[
{ required: true, message: `${title} is required.` },
// Add more validation rules as needed
]}
>
{/* 输入框,按下回车或失去焦点时保存 */}
<Input onPressEnter={save} onBlur={save} />
</Form.Item>
</Form>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
)
}
return <td {...restProps}>{childNode}</td>
}
/** 点击可编辑表单 */
const EditableTable = () => {
const [dataSource, setDataSource] = useState([
{
key: '0',
name: 'Edward King 0',
age: '32',
address: 'London, Park Lane no. 0',
},
{
key: '1',
name: 'Edward King 1',
age: '42',
address: 'London, Park Lane no. 1',
},
])
/** 保存编辑后的行数据 */
const handleSave = (row) => {
const newData = [...dataSource]
const index = newData.findIndex((item) => row.key === item.key)
const item = newData[index]
newData.splice(index, 1, { ...item, ...row })
setDataSource(newData)
}
// 表格列配置
const columns = [
{
title: 'Name',
dataIndex: 'name',
width: '30%',
editable: true, // 可编辑
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
editable: true,
},
{
title: 'Operation',
dataIndex: 'operation',
render: (_, record) =>
dataSource.length >= 1 ? (
<Popconfirm
title="Sure to delete?"
onConfirm={() => handleDelete(record.key)}
>
<a>Delete</a>
</Popconfirm>
) : null,
},
]
const handleDelete = (key) => {
const newData = dataSource.filter((item) => item.key !== key)
setDataSource(newData)
}
/** 合并列,添加单元格属性 */
const mergedColumns = columns.map((col) => {
if (!col.editable) {
return col
}
return {
...col,
onCell: (record) => ({
// 设置单元格属性
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave,
}),
}
})
return (
<Table
components={{
body: {
cell: EditableCell, // 使用可编辑单元格组件
},
}}
bordered
dataSource={dataSource}
columns={mergedColumns}
rowClassName="editable-row"
pagination={false} // 不使用分页
/>
)
}
export default EditableTable
总体来说,react 还是更加灵活多变。