这个项目得以实现,要感谢所有支持我的工作的 Sponsors
您可以在我的赞助商资料中加入他们:
English | 简体中文
表单状态管理和数值校验
在 Vue 项目中,我们经常会使用各种预制的表单组件,它可能是自己编写的,或者来自第三方 UI 库。对于第三方 UI 库而言,它们可能有自己的表单校验器,而自己的组件则需要自己编写表单校验器,很多时候这些表单的校验器是不统一的,尤其是当你在项目中混合使用第三方 UI 库 和自己的组件时,将它们结合在一起会十分困难。
基于现代 CSS 工具类和组件化,编写自己风格的 <input>
组件并将它们组合成表单十分容易,然而在涉及到需要整合所有输入相关的表单状态管理和规则校验时,问题就变得复杂起来了。
因此我开始为满足这一需求而对一个解决方案进行实验,并将其取名为 SlimeForm,含义为像史莱姆一样嵌入并粘粘所有表单 💙。
SlimeForm 是一个无组件、无内置规则的表单状态管理器和验证器,通过 v-model
绑定所有的原生或自定义组件,并响应式地管理以及验证它们。
- 改进功能
- 使用
reactive
类型返回表单 - 对于单个规则,可以省略数组
- 使用
status[key].isDirty
标记表单的值是否被修改
- 使用
- 文档
- 更好的 Typescript 类型定义
- 单元测试
- 添加对
object
类型字段的支持 - 添加对异步规则的支持
- 支持过滤未修改的条目,只留下已经修改的条目进行提交
- 支持第三方规则,比如 yup
- 支持
validateSync
- 支持
validate
(异步)
- 支持
- 💡 更多的点子
⚗️ 实验性
npm i slimeform
SlimeForm 仅支持 Vue 3
将 form
用 v-model
绑定到 <input>
或是其他组件。
值改变时 status
会产生对应的变化;使用 reset 方法重置表单的值到初始状态。
<script setup>
import { useForm } from 'slimeform'
const { form, status, reset, dirtyFields } = useForm({
// 初始的 form 值
form: () => ({
username: '',
password: '',
}),
})
</script>
<template>
<form @submit.prevent="mySubmit">
<label>
<!-- 这里 -->
<input v-model="form.username" type="text">
<input v-model="form.password" type="text">
</label>
<button type="submit">
提交
</button>
</form>
</template>
const { form, status, reset, isDirty } = useForm(/* ... */)
// 表单是否已经被修改
isDirty.value
// username 是否已经被修改
status.username.isDirty
// password 是否已经被修改
status.password.isDirty
// 重置表单, 恢复到初始状态
reset()
// 重置指定字段
reset('username', 'password', /* ... */)
useForm
的表单初始值可以是其它变量或 pinia 的状态,初始值的改变将在表单重置时同步到 form
对象中
const userStore = useUserStore()
const { form, reset } = useForm({
form: () => ({
username: userStore.username,
intro: userStore.intro,
}),
})
// 假设我们调用 setInfo 函数更新了 username 和 intro 的值
userStore.setInfo(/* ... */)
// 在调用 reset 时,对 `userStore` 所做的更改将同步到 `form` 对象中
reset()
// 这些属性将是之前调用过 `setInfo` 之后 `userStore` 的值
form.username
form.intro
假设你正在开发一个对现有数据进行编辑的表单,用户通常只会修改其中的部分字段,然后前端会通过 HTTP PATCH 将用户修改过的那部分字段提交给后端,后端依据提交了哪些字段进行局部更新
此类需求可以使用 dirtyFields
计算函数,它的值是一个只会包含 form
当中被修改过字段的对象。
const { form: userInfo, status, dirtyFields } = useForm(/* ... */)
dirtyFields.value /* value: {} */
// 编辑用户简介
userInfo.intro = 'abcd'
dirtyFields.value /* value: { intro: 'abcd' } */
// 编辑用户简介为默认值
userInfo.intro = '' /* default value */
dirtyFields.value /* value: {} */
使用 rule
定义表单字段的验证规则。当字段值发生更改时,验证过程将自动进行,验证结果将会存储和呈现在 status[key].isError
和 status[key].message
属性中。
如果一个字段需要多个规则,可以使用函数数组来声明。
你可以维护自己的规则集,并在需要使用的地方导入。
// formRules.ts
export function isRequired(value) {
if (value && value.trim())
return true
return t('required') // i18n 支持
}
<script setup>
import { isRequired } from '~/util/formRules.ts'
const {
form,
status,
submitter,
clearErrors,
isError,
verify
} = useForm({
// 初始 form 值
form: () => ({
name: '',
age: '',
}),
// 进行校验
rule: {
name: isRequired,
// 如果一个字段有多条规则,可以使用数组
age: [
isRequired,
// 要求字段是数字 number
val => !Number.isNaN(val) || 'Expected number',
// 要求字段有最大值
val => val.length < 3 || 'Length needs to be less than 3',
],
},
})
const { submit } = submitter(() => {
alert(`Age: ${form.age} \n Name: ${form.name}`)
})
</script>
<template>
<form @submit.prevent="submit">
<!-- ... -->
</form>
</template>
此外,您可以在验证错误消息中使用任何响应式的值,例如如上所示,对 vue-i18n
库的多语言函数 t('required')
的调用。
手动触发校验
const { _, status, verify } = useForm(/* ... */)
// 表单校验
verify()
// 字段校验
status.username.verify()
手动指定错误
status.username.setError('username has been registered')
清除错误
const { _, status, clearErrors, reset } = useForm(/* ... */)
// 清除字段的错误
status.username.clearError()
// 清除全部错误
clearErrors()
// 重置表单也会清除错误
reset()
任何错误
isError
: 是否有任何表单字段包含错误的验证结果
const { _, isError } = useForm(/* ... */)
isError /* true / false */
表单校验信息占位内容
使用 defaultMessage
定义表单字段校验信息的占位内容。默认值为 ''
,你可以将它设置为 \u00A0
,在渲染时会被转义为
,以此来避免没有 message 时 <p>
出现高度坍塌问题。
const { form, status } = useForm({
form: () => ({/* ... */}),
rule: {/* ... */},
// 没有错误消息时的占位内容
defaultMessage: '\u00A0',
})
规则懒校验
将 lazy
设置为 true
可以阻止数据变化时规则自动校验。
此时, 可以考虑调用 verify()
或 status[fieldName].verify()
来手动校验字段。
const { form, status, verify } = useForm({
form: () => ({
userName: '',
/* ... */
}),
rule: {
userName: v => v.length < 3,
},
lazy: true,
})
form.userName = 'abc'
status.userName.isError // false
verify()
status.userName.isError // true
useForm()
返回的 rule
Slimeform 在 useForm()
的返回值中提供了 rule
, 该字段可以校验表单外的数据。如果您想确保数据在传入表单之前一定是满足规则的,那么这个字段是很有用的。
const { form, rule } = useForm({
form: () => ({
userName: '',
/* ... */
}),
rule: {
userName: v => v.length < 3 || 'to many characters',
},
})
const text = 'abcd'
const isValid = rule.userName.validate(text) // false
if (isValid)
form.userName = text
您还可以获取到该字段的错误信息,方法是在第二个选项参数里传入 fullResult: true
。此时该方法会返回一个带有错误信息的对象。
rule.userName.validate('abcd', { fullResult: true }) // { valid: false, message: "to many characters" }
rule.userName.validate('abc', { fullResult: true }) // { valid: true, message: null }
submitter
接受一个回调函数参数,返回触发这个回调函数的函数和表示函数运行中的状态变量;传入 submitter
的回调函数可以拿到 useForm
函数返回的所有状态和函数,这样可以将回调函数放到单独的代码中,甚至编写通用的提交函数,方便组合使用。
<script setup>
import { useForm } from 'slimeform'
const { _, submitter } = useForm(/* ... */)
// Define the submit function
const {
// trigger submit callback
submit,
// Indicates whether the asynchronous commit function is executing
submitting,
} = submitter(async ({ form, status, isError, reset /* ... */ }) => {
// Submission Code
const res = await fetch(/* ... */)
// ....
})
</script>
<template>
<form @submit.prevent="submit">
<!-- ... -->
<!-- Use `submitting` to disable buttons and add loading indicator -->
<button type="submit" :disabled="submitting">
<icon v-if="submitting" name="loading" />
Submit
</button>
</form>
</template>
默认情况下调用 submit
函数后会先进行表单规则验证,如果验证失败,将会直接结束函数执行,如果需要关闭此行为可以在 submitter
函数的第二个参数参数 options
中配置 enableVerify: false
来跳过验证。
import { mySubmitForm } from './myFetch.ts'
const { _, submitter } = useForm(/* ... */)
// Wrap the generic submission code and use it later
const { submit, submitting } = submitter(mySubmitForm({ url: '/register', method: 'POST' }))
如果你不想自己编写验证规则的细节,已经有一种非常简洁的方法可以使用 Yup 作为规则。
SlimeForm 内置了 Yup 同步规则的解析器:yupFieldRule
,你可以从 slimeform/resolvers
导入它。yupFieldRule
函数在内部调用 schema.validateSync
方法,并处理结果为 SlimeForm 可接受的格式。
首先,你要安装 Yup
npm install yup
然后在代码中导入 yup
和 yupFieldRule
就可以使用了
import { useForm } from 'slimeform'
import * as yup from 'yup'
/* 导入解析器 */
import { yupFieldRule } from 'slimeform/resolvers'
const { t } = useI18n()
const { form, status } = useForm({
form: () => ({ age: '' }),
rule: {
/* 一些使用案例 */
age: [
yupFieldRule(yup.string()
.required(),
),
yupFieldRule(yup.number()
.max(120, () => t('xxx_i18n_key'))
.integer()
.nullable(),
),
],
},
})
一些建议:
- 使用
@submit.prevent
而不是@submit
来屏蔽表单默认提交行为 - 使用
isError
的值来动态地判断是否需要给表单输入框添加红色的描边
<template>
<h3>请输入你的年龄</h3>
<form @submit.prevent="submitFn">
<label>
<input
v-model="form.age"
type="text"
:class="status.age.isError && '!border-red'"
>
<p>{{ status.age.message }}</p>
</label>
<button type="submit">
提交
</button>
</form>
</template>