Skip to content

FrameMuse/react-zod-form

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Zod Form

Simple form handling with full validation control.

Navigation

Other approaches

Read this article to know how to choose your form library.

Install

npm

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, begin with it first.

You start from creating new ZodForm

import ZodForm from "react-zod-form"

const form = new ZodForm()

Then you declare some fields

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

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)

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, ...)

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"

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

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

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

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

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

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.