react-zod-form
Version:
Simple form validation and values transformation.
473 lines (358 loc) • 13.7 kB
Markdown
# React Zod Form
Simple form handling with full validation control.
<!-- Form handling, relying on zod schemas.
This provides super mega simplicity in the code and gives the same amount of control over the forms. -->
## Navigation
- [React Zod Form](#react-zod-form)
- [Navigation](#navigation)
- [Other approaches](#other-approaches)
- [Install](#install)
- [npm](#npm)
- [Features](#features)
- [Usage](#usage)
- [Getting values](#getting-values)
- [Handling issues](#handling-issues)
- [Field value types](#field-value-types)
- [Transform rules](#transform-rules)
## Other approaches
[Read this article](https://retool.com/blog/choosing-a-react-form-library/) to know how to choose your form library.
- [React hook form](https://react-hook-form.com/)
- [Formik](https://formik.org/)
- [React-final-form](https://final-form.org/react)
## Install
### npm
```bash
npm i react-zod-form
```
## Features
- Zod
- Field value parser by apparent rules
- Field names helper
## Usage
### Getting values
_If you're not familiar with [`Zod`](https://zod.dev/), begin with it first._
You start from creating new `ZodForm`
```ts
import ZodForm from "react-zod-form"
const form = new ZodForm()
```
Then you declare some fields
```ts
import ZodForm from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
```
You just created Zod form!
**Notice:** _It's better to keep zod form in the low level to make sure you're creating ZodForm only once._
---
Now let's create react form component
```tsx
function ExampleForm() {
return (
<form>
<input placeholder="Enter your username" required />
<input placeholder="Enter your email" type="email" required />
<input placeholder="Enter your website" type="url" required />
</form>
)
}
export default ExampleForm
```
Combine zod schema and give fields their names (help yourself with `fields`)
```tsx
import ZodForm from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
function ExampleForm() {
return (
<form>
<input placeholder="Enter your username" required name={form.fields.username} />
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
Now let's get some values on event (i.e. `onBlur`, `onFocus`, `onChange`, `onSubmit`, ...)
```tsx
import { FormEvent } from "react"
import ZodForm from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
function ExampleForm() {
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return a field that was currently unfocused, otherwise throws error
const field = form.parseCurrentField(event)
console.log(field.name, field.value)
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
console.log(fields)
}
return (
<form onBlur={onBlur}>
<input placeholder="Enter your username" required name={form.fields.username} />
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
Wow, now you have your fields just in a few lines of code and it's all concise!
**Notice:** _There is a safe version of `parseAllFields` - `safeParseAllFields`, works just same as in zod._
Let's talk about form interface as you may want your form to be a "standonle module"
```tsx
import { FormEvent } from "react"
import ZodForm from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
export type ExampleFormFields = z.infer<typeof form.object>
interface ExampleFormProps {
onBlur?(value: ExampleFormFields): void
}
function ExampleForm(props: ExampleFormProps) {
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return a field that was currently unfocused, otherwise throws error
const field = form.parseCurrentField(event)
console.log(field.name, field.value)
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
console.log(fields)
}
return (
<form onBlur={onBlur}>
<input placeholder="Enter your username" required name={form.fields.username} />
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
I'll explain you this a bit:
Interface `ExampleFormFields` represents a value that your output will give, your output is to be methods like `onBlur`, `onFocus`, `onChange`, `onSubmit`.
### Handling issues
Issues are form errors. This is a separate module, so you need to import this along with `ZodForm`.
Let's take the previous example and start with reporting and clearing error
```tsx
import { FormEvent } from "react"
import ZodForm, { useZodFormIssues } from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
export type ExampleFormFields = z.infer<typeof form.object>
interface ExampleFormProps {
onBlur?(value: ExampleFormFields): void
}
function ExampleForm(props: ExampleFormProps) {
const { reportError, clearError } = useZodFormIssues(form)
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
if (fields.success) {
clearError() // You better clear error right after success check
console.log(fields)
} else {
reportError(fields.error)
}
}
return (
<form onBlur={onBlur}>
<input placeholder="Enter your username" required name={form.fields.username} />
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
Now continue with displaying issue message
```tsx
import { FormEvent } from "react"
import ZodForm, { useZodFormIssues } from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
export type ExampleFormFields = z.infer<typeof form.object>
interface ExampleFormProps {
onBlur?(value: ExampleFormFields): void
}
function ExampleForm(props: ExampleFormProps) {
const { reportError, clearError, fieldIssues } = useZodFormIssues(form)
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
if (fields.success) {
clearError() // You better clear error right after success check
console.log(fields)
} else {
reportError(fields.error)
}
}
return (
<form onBlur={onBlur}>
{fieldIssues.username}
<input placeholder="Enter your username" required name={form.fields.username} />
{fieldIssues.email}
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
{fieldIssues.url}
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
**Notice:** _`fieldIssues` will have the same keys as your form fields._
---
Let's say all your form fields are valid, but something went wrong on a backend
```tsx
import { FormEvent } from "react"
import ZodForm, { useZodFormIssues } from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
export type ExampleFormFields = z.infer<typeof form.object>
interface ExampleFormProps {
onBlur?(value: ExampleFormFields): void
}
function ExampleForm(props: ExampleFormProps) {
const { reportError, clearError, fieldIssues, addIssue } = useZodFormIssues(form)
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
if (fields.success) {
clearError() // You better clear error right after success check
requestBackend(fields.data)
} else {
reportError(fields.error)
}
}
/**
* --- THIS IS EXAMPLE ---
*/
function requestBackend(fields: ExampleFormFields) {
const response = send(fields)
if (!response.error) return
response.error.fields.forEach(field => {
addIssue([field.name], field.message)
})
}
return (
<form onBlur={onBlur}>
{fieldIssues.username}
<input placeholder="Enter your username" required name={form.fields.username} />
{fieldIssues.email}
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
{fieldIssues.url}
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
```
<!-- ### Observing dirty
Finding out if a form has been changed.
```tsx
import { FormEvent } from "react"
import ZodForm, { useZodFormIssues } from "react-zod-form"
import { z } from "zod"
const form = new ZodForm({
userName: z.string().min(3, "Enter at least 3 chars"),
email: z.string().email("Follow this email format: email@example.com"),
website: z.string().url("Follow this URL format: https://example.com")
})
export type ExampleFormFields = z.infer<typeof form.object>
interface ExampleFormProps {
onBlur?(value: ExampleFormFields): void
}
function ExampleForm(props: ExampleFormProps) {
const { observe, isDirty } = useZodFormDirty(form, {/* Your default state */})
/**
* Triggered on input unfocus.
*/
function onBlur(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
// Tries to return all field values, otherwise throws error
const fields = form.parseAllFields(event)
if (fields.success) {
clearError() // You better clear error right after success check
console.log(fields)
} else {
reportError(fields.error)
}
}
return (
<form onBlur={onBlur}>
{fieldIssues.username}
<input placeholder="Enter your username" required name={form.fields.username} />
<input placeholder="Enter your email" type="email" required name={form.fields.email} />
<input placeholder="Enter your website" type="url" required name={form.fields.url} />
</form>
)
}
export default ExampleForm
``` -->
## Field value types
A form field value may be one of these type (can be in array):
- `string`
- `number`
- `boolean`
- `File`
Take a look at the interface to understand it better
```ts
type FormFieldValue = FormFieldValueBasic | FormFieldValueBasic[]
type FormFieldValueBasic = string | number | boolean | File
```
## Transform rules
| Type | Output | Description |
| :-----------------------: | :--------------: | --------------------------------------------------------------------------------------------------------- |
| `any` | `number` | If value is a parsable number, it will be converted to `number`. |
| `any` | `boolean` | If value is `"true"` or `"false"`, it will be converted to `boolean`. |
| `"radio"` \| `"checkbox"` | `boolean` | If value is `"ok"` and type is `"radio" \| "checkbox"`, the value from `checked` attribute will be taken. |
| `"file"` | `File \| File[]` | If type is `"file"`, it will give `File` or `File[]`, depending on `multiple` attribute. |