表单校验总结

Huy大约 9 分钟javascriptjavascript

本文将梳理一遍表单的各种校验方法。先从原生校验进行总结,再延伸到 vue 和 Rect 的表单校验。

form 表单原生校验

在 HTML5 中,表单校验有原生的支持,这些校验功能可以在不借助 JavaScript 的情况下进行基本的验证。

  1. 必填字段 (required),使用 required 属性可以指定某个字段是必填的。

    <form>
      <label for="username">Username:</label>
      <input type="text" id="username" name="username" required />
      <input type="submit" value="Submit" />
    </form>
    
  2. 输入类型 (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
      />
      
  3. 输入字段 (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>
    
  4. 输入字段的长度范围 (minlength, maxlength),可以指定文本字段的最小和最大输入长度。

    <label for="username">Username:</label>
    <input
      type="text"
      id="username"
      name="username"
      minlength="5"
      maxlength="15"
      required
    />
    
  5. 输入字段的数据范围 (min, max),用于 numberdate 类型字段,指定允许的数值或日期范围。

    <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"
    />
    
  6. 自定义错误消息(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。表单元素的各项验证条件,包括:

  1. 填字段 (required 属性)
  2. 类型 (type 属性,例如 email, number 等)
  3. 模式 (pattern 属性,使用正则表达式验证)
  4. 最小长度和最大长度 (minlength 和 maxlength 属性)
  5. 范围 (min 和 max 属性)
  6. 步长 (step 属性)
  7. 自定义的验证消息(通过 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-validatoropen in new window 库进行校验。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 的自定义校验数组定义可参考官网open in new window,本质上还是更贴近 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 还是更加灵活多变。

Loading...