This project is made possible by all the sponsors supporting my work
You can join them at my sponsors profile:
English | 简体中文
Form state management and validation
We usually use all sorts of different pre-made form components in vue projects which may be written by ourselves, or come from other third-party UI libraries. As for those third-party UI libraries, they might shipped their own form validators with libraries, however we still will need to build our form validators for those components written by us. In most of the time, those form validators were not 'unified' or we say compatible to the others, especially when you mixed your own components with third-party components together in one project where thing might become tricky.
Base on modern CSS utilities class and component-based design, it has now become way more easier to write your own <input>
component in specific style and assemble them as a form, however, when you need to integrate form state management and rule validation with all the related input fields, the problem will be more complex.
So I started to experiment a solution to achieve this kind of functionalities, and naming it with SlimeForm, which means this utilities would try it best to fit in the forms just like the slime does 💙.
SlimeForm is a form state management and validator which is dependency free, no internal validation rules shipped and required. By binding all native or custom components through v-model
, SlimeForm is able to manage and validate values reactively.
- Improve the functionalities
- Use reactive type to return the form
- For a single rule, the array can be omitted
- Mark whether the value of the form has been modified
- Documentations
- Better type definitions for Typescript
- Unit tests
- Add support to fields with
object
type - Add support to async rule validation
- Support filtering the unmodified entries in the form, leaving only the modified entries for submission
- Support for third-party rules, such as yup
- Support
validateSync
- Support
validate
(Async)
- Support
- 💡 More ideas...
⚗️ Experimental
npm i slimeform
SlimeForm only works with Vue 3
Use v-model
to bind form[key]
on to the <input>
element or other components.
status
value will be changed corresponded when the form values have been modified. Use the reset
function to reset the form values back to its initial states.
<script setup>
import { useForm } from 'slimeform'
const { form, status, reset, dirtyFields } = useForm({
// Initial form value
form: () => ({
username: '',
password: '',
}),
})
</script>
<template>
<form @submit.prevent="mySubmit">
<label>
<!-- here -->
<input v-model="form.username" type="text">
<input v-model="form.password" type="text">
</label>
<button type="submit">
Submit
</button>
</form>
</template>
const { form, status, reset, isDirty } = useForm(/* ... */)
// whether the form has been modified
isDirty.value
// whether the username has been modified
status.username.isDirty
// whether the password has been modified
status.password.isDirty
// Reset form, restore form values to default
reset()
// Reset the specified fields
reset('username', 'password', /* ... */)
The initial states of useForm
could be any other variables or pinia states. The changes made to the initial values will be synced into the form
object when the form has been resetted.
const userStore = useUserStore()
const { form, reset } = useForm({
form: () => ({
username: userStore.username,
intro: userStore.intro,
}),
})
// update the value of username and intro properties
userStore.setInfo(/* ... */)
// changes made to the `userStore` will be synced into the `form` object,
// when reset is being called
reset()
// these properties will be the values of `userStore` where `setInfo` has been called previously
form.username
form.intro
Suppose you are developing a form to edit existing data, where the user usually only modifies some of the fields, and then the front-end submits the modified fields to the back-end via HTTP PATCH to submit the user-modified part of the fields to the backend, and the backend will partially update based on which fields were submitted
Such a requirement can use the dirtyFields
computed function, whose value is an object that only contains the modified fields in the form
.
const { form: userInfo, status, dirtyFields } = useForm(/* ... */)
dirtyFields.value /* value: {} */
// Edit user intro
userInfo.intro = 'abcd'
dirtyFields.value /* value: { intro: 'abcd' } */
// Edit user profile to default
userInfo.intro = '' /* default value */
dirtyFields.value /* value: {} */
Use rule
to define the validation rules for form fields. The verification process will be take placed automatically when values of fields have been changed, the validation result will be stored and provided in status[key].isError
and status[key].message
properties. If one fields requires more than one rule, it can be declared by using function arrays.
You can also maintain your rule collections on your own, and import them where they are needed.
// formRules.ts
function isRequired(value) {
if (value && value.trim())
return true
return t('required') // i18n support
}
<script setup>
import { isRequired } from '~/util/formRules.ts'
const {
form,
status,
submitter,
clearErrors,
isError,
verify
} = useForm({
// Initial form value
form: () => ({
name: '',
age: '',
}),
// Verification rules
rule: {
name: isRequired,
// If one fields requires more then one rule, it can be declared by using function arrays.
age: [
isRequired,
// is number
val => !Number.isNaN(val) || 'Expected number',
// max length
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>
In addition, you can use any reactive values in the validation error message, such as the t('required')
function call from vue-i18n
as the examples shown above.
Manually trigger the validation
const { _, status, verify } = useForm(/* ... */)
// validate the form
verify()
// validate individual fields
status.username.verify()
Manually specify error message
status.username.setError('username has been registered')
Maunally clear the errors
const { _, status, clearErrors, reset } = useForm(/* ... */)
// clear the error for individual field
status.username.clearError()
// clear all the errors
clearErrors()
// reset will also clear the errors
reset()
Any errors
isError
: Are there any form fields that contain incorrect validation results
const { _, isError } = useForm(/* ... */)
isError /* true / false */
Default message for form
Use defaultMessage
to define a placeholders for the form field validation error message. The default value is ''
, you can set it to \u00A0
, which will be escaped to
during rendering, to avoid the height collapse problem of <p>
when there is no messages.
const { form, status } = useForm({
form: () => ({/* ... */}),
rule: {/* ... */},
// Placeholder content when there are no error message
defaultMessage: '\u00A0',
})
Lazy rule validation
You can set lazy
to true
to prevent rules from being automatically verified when data changes.
In this case, consider call verify()
or status[fieldName].verify()
to manually validate fields.
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
rule
in return value of useForm()
Slimeform provides rule
in return value of useForm()
, which can be used to validate data not included in form. This can be useful if you want to make sure anything passing into form is valid.
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
You can also get access to the error message by indicating fullResult: true
in the second options argument, in which case an object containing the message will be returned.
rule.userName.validate('abcd', { fullResult: true }) // { valid: false, message: "to many characters" }
rule.userName.validate('abc', { fullResult: true }) // { valid: true, message: null }
submitter
accepts a callback function as argument which returns the function that be able to triggered this callback function and a state variable that indicates the function is running. The callback function passed into submitter
can get all the states and functions returned by the useForm
, which allows you to put the callback function into separate code or even write generic submission functions for combination easily.
<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>
By default, the form rules validation will take place first after the submit
function have been called, if the validation failed, the function call will be terminated immediately.
If you want to turn off this behavior, you can configure enableVerify: false
option in the second parameter options
of the submitter
to skip the validation.
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' }))
If you don't want to write the details of validation rules yourself, there is already a very clean way to use Yup as a rule.
SlimeForm has a built-in resolvers for Yup synchronization rules: yupFieldRule
, which you can import from slimeform/resolvers
. yupFieldRule
function internally calls schema.validateSync
method and processes the result in a format acceptable to SlimeForm.
First, you have to install Yup
npm install yup
then import yup
and yupFieldRule
into your code and you're ready to go!
import { useForm } from 'slimeform'
import * as yup from 'yup'
/* Importing a resolvers */
import { yupFieldRule } from 'slimeform/resolvers'
const { t } = useI18n()
const { form, status } = useForm({
form: () => ({ age: '' }),
rule: {
/* Some use cases */
age: [
yupFieldRule(yup.string()
.required(),
),
yupFieldRule(yup.number()
.max(120, () => t('xxx_i18n_key'))
.integer()
.nullable(),
),
],
},
})
Some suggestions:
- Use
@submit.prevent
instead of@submit
, this can prevent the submitting action take place by form's default - Use
isError
to determine whether to add a red border around the form dynamically
<template>
<h3>Please enter your age</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">
Submit
</button>
</form>
</template>