Formstate nuxt plugin
npm install @formstate/nuxtformstate is a small vue3 library that makes it easy to handle your form state and perform validations. It features:
- A small footprint (2.7kb gzip)
- No tight coupling with inputs, you have all the liberty to build your own inputs
- Full Typescript support, with returned fields typed based on your initial state
- Easy zod or other validation libraries support
- You can directly manipulate your field values (e.g. value, dirty, focused etc)
- Shared state of useForm -> you can reuse the same form in another component by naming your form
- Async validation support
- Custom error objects, you can return any object from a validation rule
For a vue 3 project:
``sh`
npm install @formstate/core
yarn add @formstate/core
pnpm add @formstate/core
For nuxt:
`sh`
npm install @formstate/core @formstate/nuxt
yarn add @formstate/core @formstate/nuxt
pnpm add @formstate/core @formstate/nuxt
And add the @formstate/nuxt package to your modules in nuxt.config.ts:
`typescript`
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ["@formstate/nuxt"],
});
`typescript
`
`typescript
Input is dirty: {{ someText.dirty }}
Input is valid: {{ someText.valid }}
Complete state: {{ formState }}
`
The formState ref contains the complete state of the form:
`typescript
console.log(formState.value)
// result:
{
"context": {},
"dirty": false,
"fields": {
"someText": {
"blur": [Function],
"dirty": false,
"errors": [],
"focus": [Function],
"focused": false,
"name": "someText",
"pending": false,
"reset": [Function],
"touched": false,
"valid": true,
"validate": [Function],
"value": "initial value",
},
},
"initialFields": {
"numberInput": {
"rules": [Function],
"value": 2,
},
"someText": "hello",
},
"pending": false,
"touched": false,
"valid": true,
}
`
We can also get all the inputs with their values from the form as a ref:
`typescript
const { values } = useForm({
someText: "initial value",
checkboxes: ["Jack"],
});
console.log(values.value);
// output: { "someText": "initial value", "checkbox": ["Jack"] }
`
Anything you return from a rule function means that the field is invalid. If you return nothing, undefined or false, it will be considered valid.
`typescript
// if you false, it will generate an error message of required
const isRequired = (value: string) => !!value;
function rule(value: string, fieldName: string) {
if (value.length > 5) {
return "requires more than 5 characters";
}
// or return array
if (value.length > 5) {
return ["requires more than 5 characters", "some other error"];
}
// or any custom object
if (value.length > 5) {
return { error: "requires more than 5 characters", code: "too short" };
}
}
// or do something like this
function rule(value: string) {
let errors: string[] = [];
if (value.length < 5) {
errors.push("min 5 chars");
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errors.push("invalid email");
}
return errors;
}
`
We can use the _setRules_ function to add rules in one go:
`typescript
const { someRadio, someTextInput, formState, setRules } = useForm({
someText: ""
someRadio: "John",
});
setRules( {
someTextInput: [isRequired, validateIfJohn],
someRadio: [validateIfJohn],
});
`
You can also set the rules directly on the fields:
`typescript
import { isRequired, isNotJohn } from '../some-validations'
const { someRadio } = useForm({
someText: ""
someRadio: "John",
});
someText.rules = [isRequired, isNotJohn]
`
We can add a rule later on as well:
`typescript`
// set a new array on .rules, push doesnt work
someText.rules = [...someText.rules, someOtherValidation];
- If no rule is present, the field is considered valid
- By default rules are validated when a field value changes
- You can customize when a validation is performed, see // TODO add link to custom validation behaviour
Sometimes we want to validate a group of fields, or have rules depend on each other. You can set the _formRules_ property on the formState object to do this. Form rules work in addition
to field rules and produce extra errors.
`typescript
const formRule = () => {
if (someTextInput.value === "John" && someRadio.value === "John") {
return "John is not allowed on both fields";
}
};
formState.value.formRules = [formRule];
// or
setRules({
someTextInput: [required],
formRules: [formRule],
});
`
Because validations are just simple functions, you can use any validation library you'd like
`typescript
const MyFormValidation = z.object({
someTextInput: z.string().min(1),
someRadio: z.string(),
someRange: z.union([z.string(), z.number()]),
});
type MyForm = z.infer
// formState type is defered from zod
const { formState, setRules } = useForm
someTextInput: "",
someRadio: "John",
someRange: 5,
});
const zodValidation = (value: unknown, name: string) => {
// we pick the value from zod, as we don't want to validate whole formstate
const result = ZodType.pick({ [name]: true }).safeParse({ [name]: value });
if (!result.success) {
return result.error.errors;
}
};
setRules({
someTextInput: [zodValidation],
someRadio: [zodValidation],
someRange: [zodValidation],
});
`
formstate has async validation built in. You can add an async function to a field or use async validation as a form rule.
`typescript
const userNameExists = async (value: string) => {
const exists = await callToApi(value);
if (exists) {
return "Username already exists";
}
};
setRules({ username: [userNameExists] });
`
Most often don't want to do a request on each change of a text field, as it would mean many calls to your api. You can debounce a rule, as long as the rule always returns a promise.
`typescript
import debounce from "debounce-promise";
const someAsyncValidation = debounce(async (value: string) => {
// if length is less than 5 characters, don't do a request
if (value.length < 5) {
return "username should be longer than 5 characters";
}
const exists = await callToApi(value);
if (exists) {
return "Username already exists";
}
}, 500);
setRules({ username: [someAsyncValidation] });
`
By default, validations on a field are performed when the field value changes. You can turn off this behaviour and write your own implementation when validation is performed:
`typescript
setRules({
someTextInput: [{ rule: someAsyncValidation, autoValidate: false }],
});
watch(
() => someTextInput.focused,
async () => {
if (!someTextInput.focused) {
await someTextInput.validate();
}
}
);
`
TODO
`typescript
const { someText, setFields } = useForm({
someText: "",
});
// editing .value will change dirty state and perform validation
someText.value = "hello world!";
// using setFields function will not change dirty or perform validation
setFields({ someText: "hello world!" });
// you can also change this behavior:
setFields({ someText: "hello world!" }, { setDirty: true, validate: true });
// other values can also be changed
someText.dirty = true;
someText.touched = true;
someText.focused = true;
someText.pending = true;
someText.valid = true;
`
`typescript
const { formState } = useForm({
someText: "",
});
formState.value.dirty = true;
formState.value.pending = true;
formState.value.touched = true;
formState.value.valid = true;
`
Often we would like to populate a form from an async request. The easiest way is to wrap the component were the form is located in a Suspense and async/await for the data:
`typescript
const data: ResponseType = await anAsyncRequest();
const { someInput } = useForm(data);
`
We can also use the _setFields_ function to later update the data
`typescript
const { numberInput, textInput, setFields } = useForm({
numberInput: 0,
textInput: "",
});
onMounted(async () => {
// data would be something like { numberInput: 6, textInput: 'hello world' }
const data: ResponseType = await anAsyncRequest();
setFields(data);
});
`
Every input has touched and boolean properties. But in order to automatically update these properties we need to attach the input events to the formState. Note that in some browsers (looking at you safari), some types of inputs (e.g. radio) do not trigger the focus events.
`typescript
type="text"
v-model="someText.value"
@focus="someText.focus"
@blur="someText.blur"
/>
`
Submitting your form can be as simple as:
`typescript
const { values } = useForm({ someTextInput: "hello" });
async function submit() {
const { valid, errors, errorFields } = await validateForm();
if (valid) {
await yourPostRequest(values);
}
}
`
Or you can choose to use the \