• Stars
    star
    714
  • Rank 63,413 (Top 2 %)
  • Language
    TypeScript
  • License
    Other
  • Created almost 3 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

πŸͺ± Zorm - Type-safe <form> for React using Zod

React Zorm

Type-safe <form> for React using Zod!

Features / opinions

  • πŸ”₯ NEW experimental automatic progressive HTML attributes helper
  • πŸ’Ž Type-safe
    • Get form data as a typed object
    • Typo-safe name and id attribute generation
  • 🀯 Simple nested object and array fields
    • And still type-safe!
  • βœ… Validation on the client and the server
    • With FormData or JSON
    • Eg. works with any JavaScript backend
    • Remix, Next.js, Express, Node.js, CF Workers, Deno etc.
  • πŸ“¦ Tiny: Less than 3kb (minified & gzipped)
    • 🌳 Tree shakes to be even smaller!
    • 🀷 No dependencies, only peer deps for React and Zod
  • πŸ›‘ No controlled inputs or context providers required
    • ☝️ The form is validated directly from the <form> DOM element
    • πŸš€ As performant as React form libraries can get!

If you enjoy this lib a Twitter shout-out @esamatti is always welcome! 😊

You can also checkout my talk at React Finland 2022. Slides.

Install

npm install react-zorm

Example

Also on Codesandbox!

import { z } from "zod";
import { useZorm } from "react-zorm";

const FormSchema = z.object({
    name: z.string().min(1),
    password: z
        .string()
        .min(10)
        .refine((pw) => /[0-9]/.test(pw), "Password must contain a number"),
});

function Signup() {
    const zo = useZorm("signup", FormSchema, {
        onValidSubmit(e) {
            e.preventDefault();
            alert("Form ok!\n" + JSON.stringify(e.data, null, 2));
        },
    });
    const disabled = zo.validation?.success === false;

    return (
        <form ref={zo.ref}>
            Name:
            <input
                type="text"
                name={zo.fields.name()}
                className={zo.errors.name("errored")}
            />
            {zo.errors.name((e) => (
                <ErrorMessage message={e.message} />
            ))}
            Password:
            <input
                type="password"
                name={zo.fields.password()}
                className={zo.errors.password("errored")}
            />
            {zo.errors.password((e) => (
                <ErrorMessage message={e.message} />
            ))}
            <button disabled={disabled} type="submit">
                Signup!
            </button>
            <pre>Validation status: {JSON.stringify(zo.validation, null, 2)}</pre>
        </form>
    );
}

Also checkout this classic TODOs example demonstrating almost every feature in the library and if you are in to Remix checkout this server-side validation example.

Nested data

Objects

Create a Zod type with a nested object

const FormSchema = z.object({
    user: z.object({
        email: z.string().min(1),
        password: z.string().min(8),
    }),
});

and just create the input names with .user.:

<input type="text" name={zo.fields.user.email()} />;
<input type="password" name={zo.fields.user.password()} />;

Arrays

Array of user objects for example:

const FormSchema = z.object({
    users: z.array(
        z.object({
            email: z.string().min(1),
            password: z.string().min(8),
        }),
    ),
});

and put the array index to users(index):

users.map((user, index) => {
    return (
        <>
            <input type="text" name={zo.fields.users(index).email()} />
            <input type="password" name={zo.fields.users(index).password()} />
        </>
    );
});

And all this is type checked πŸ‘Œ

See the TODOs example for more details

Server-side validation

This is Remix but React Zorm does not actually use any Remix APIs so this method can be adapted for any JavaScript based server.

import { parseForm } from "react-zorm";

export let action: ActionFunction = async ({ request }) => {
    const form = await request.formData();
    // Get parsed and typed form object. This throws on validation errors.
    const data = parseForm(FormSchema, form);
};

Server-side field errors

The useZorm() hook can take in any additional ZodIssues via the customIssues option:

const zo = useZorm("signup", FormSchema, {
    customIssues: [
        {
            code: "custom",
            path: ["username"],
            message: "The username is already in use",
        },
    ],
});

These issues can be generated anywhere. Most commonly on the server. The error chain will render these issues on the matching paths just like the errors coming from the schema.

To make their generation type-safe react-zorm exports createCustomIssues() chain to make it easy:

const issues = createCustomIssues(FormSchema);

issues.username("Username already in use");

const zo = useZorm("signup", FormSchema, {
    customIssues: issues.toArray(),
});

This code is very contrived but take a look at these examples:

The Chains

The chains are a way to access the form validation state in a type safe way. The invocation via () returns the chain value. On the fields chain the value is the name input attribute and the errors chain it is the possible ZodIssue object for the field.

There few other option for invoking the chain:

fields invocation

Return values for different invocation types

  • ("name"): string - The name attribute value
  • ("id"): string - Unique id attribute value to be used with labels and aria-describedby
  • (): string - The default, same as "name"
  • (index: number): FieldChain - Special case for setting array indices
  • (fn: RenderFunction): any - Calls the function with {name: string, id: string, type: ZodType, issues: ZodIssue} and renders the return value.

errors invocation

  • (): ZodIssue | undefined - Possible ZodIssue object
  • (value: T): T | undefined - Return the passed value on error. Useful for setting class names for example
  • (value: typeof Boolean): boolean - Return true when there's an error and false when it is ok. Example .field(Boolean).
  • <T>(render: (issue: ZodIssue, ...otherIssues: ZodIssue[]) => T): T | undefined - Invoke the passed function with the ZodIssue and return its return value. When there's no error a undefined is returned and the function will not be invoked. Useful for rendering error message components. One field can have multiple issues so to render them all you can use the spread operator ...issues.
  • (index: number): ErrorChain - Special case for accessing array elements

Using input values during rendering

The first tool you should reach is React. Just make the input controlled with useState(). This works just fine with checkboxes, radio buttons and even with text inputs when the form is small. React Zorm is not really interested how the inputs get on the form. It just reads the value attributes using the platform form APIs (FormData).

But if you have a larger form where you need to read the input value and you find it too heavy to read it with just useState() you can use useValue() from Zorm.

import { useValue } from "react-zorm";

function Form() {
    const zo = useZorm("form", FormSchema);
    const value = useValue({ zorm: zo, name: zo.fields.input() });
    return <form ref={zo.ref}>...</form>;
}

useValue() works by subscribing to the input DOM events and syncing the value to a local state. But this does not fix the performance issue yet. You need to move the useValue() call to a subcomponent to avoid rendering the whole form on every input change. See the Zorm type docs on how to do this.

Alternatively you can use the <Value> wrapper which allows access to the input value via render prop:

import { Value } from "react-zorm";

function Form() {
    const zo = useZorm("form", FormSchema);
    return (
        <form ref={zo.ref}>
            <input type="text" name={zo.fields.input()} />
            <Value form={zo.ref} name={zo.fields.input()}>
                {(value) => <span>Input value: {value}</span>}
            </Value>
        </form>
    );
}

This way only the inner <span> element renders on the input changes.

Here's a codesandox demonstrating these and vizualizing the renders.

FAQ

When Zorm validates?

When the form submits and on input blurs after the first submit attempt.

If you want total control over this, pass in setupListeners: false and call validate() manually when you need. Note that now you need to manually prevent submitting when the form is invalid.

function Signup() {
    const zo = useZorm("signup", FormSchema, { setupListeners: false });

    return (
        <form
            ref={zo.ref}
            onSubmit={(e) => {
                const validation = zo.validate();

                if (!validation.success) {
                    e.preventDefault();
                }
            }}
        >
            ...
        </form>
    );
}

How to handle 3rdparty components?

That do not create <input> elements?

Since Zorm just works with the native <form> you must sync their state to <input type="hidden"> elements in order for them to become actually part of the form.

Here's a Codesandbox example with react-select.

Another more modern option is to use the formdata event. Codesandbox example

How to validate dependent fields like password confirm?

See https://twitter.com/esamatti/status/1488553690613039108

How to translate form error messages to other languages?

Use the ZodIssue's .code properties to render corresponding error messages based on the current language instead of just rendering the .message.

See this Codesandbox example:

https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/internalization?file=/src/App.tsx

How to use checkboxes?

Checkboxes can result to simple booleans or arrays of selected values. These custom Zod types can help with them. See this usage example.

const booleanCheckbox = () =>
    z
        .string()
        // Unchecked checkbox is just missing so it must be optional
        .optional()
        // Transform the value to boolean
        .transform(Boolean);

const arrayCheckbox = () =>
    z
        .array(z.string().nullish())
        .nullish()
        // Remove all nulls to ensure string[]
        .transform((a) => (a ?? []).flatMap((item) => (item ? item : [])));

How to do server-side validation without Remix?

If your server does not support parsing form data to the standard FormData you can post the form as JSON and just use .parse() from the Zod schema. See the next section for JSON posting.

How to submit the form as JSON?

Prevent the default submission in onValidSubmit() and use fetch():

const zo = useZorm("todos", FormSchema, {
    onValidSubmit: async (event) => {
        event.preventDefault();
        await fetch("/api/form-handler", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(event.data),
        });
    },
});

If you need loading states React Query mutations can be cool:

import { useMutation } from "react-query";

// ...

const formPost = useMutation((data) => {
    return fetch("/api/form-handler", {
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
    });
});

const zo = useZorm("todos", FormSchema, {
    onValidSubmit: async (event) => {
        event.preventDefault();
        formPost.mutate(event.data);
    },
});

return formPost.isLoading ? "Sending..." : null;

How to upload and validate files?

Use z.instanceof(File) for the file input type. See this Codesandox for an example.

Native forms support files as is but if you need to POST as JSON you can turn the file to a base64 for example. See FileReader.readAsDataURL(). Or just post the file separately.

API

Tools available for importing from "react-zorm"

useZorm(formName: string, schema: ZodObject, options?: UseZormOptions): Zorm

Create a form Validator

param formName: string

The form name. This used for the input id generation so it should be unique string within your forms.

param schema: ZodObject

Zod schema to parse the form with.

param options?: UseZormOptions

  • onValidSubmit(event: ValidSubmitEvent): any: Called when the form is submitted with valid data
    • ValidSubmitEvent#data: The Zod parsed form data
    • ValidSubmitEvent#target: The form HTML Element
    • ValidSubmitEvent#preventDefault(): Prevent the default form submission
  • setupListeners: boolean: Do not setup any listeners. Ie. onValidSubmit won't be called nor the submission is automatically prevented. This gives total control when to validate the form. Set your own onSubmit on the form etc. Defaults to true.
  • customIssues: ZodIssue[]: Any additional ZodIssue to be rendered within the error chain. This is commonly used to handle server-side field validation
  • onFormData(event: FormDataEvent): Convinience callback for accessing the formdata event because React does not support it directly on the in JSX. This can be used to modify the outgoing form without modifying the form on the DOM. See this Codesandbox example on how it can used to handle controlled components.

return Zorm

  • ref: A callback ref for the <form> element
  • form: The current form element set by the callback ref
  • validation: SafeParseReturnType | null: The current Zod validation status returned by safeParse()
  • validate(): SafeParseReturnType: Manually invoke validation
  • fields: FieldChain: The fields chain
  • errors: ErroChain: The error chain

Zorm Type

The type of the object returned by useZorm(). This type object can be used to type component props if you want to split the form to multiple components and pass the zorm object around.

import type { Zorm } from "react-zorm";

function MyForm() {
    const zo = useZorm("signup", FormSchema);

    return (
        // ...
        <SubComponent zorm={zo} />
        //..
    );
}

function SubComponent(props: { zorm: Zorm<typeof FormSchema> }) {
    // ...
}

useValue(subscription: ValueSubscription): string

Get live raw value from the input.

ValueSubscription

  • form: RefObject<HTMLFormElement>: The form ref from zo.ref
  • initialValue: string: Initial value on the first and ssr render
  • transform(value: string): any: Transform the value before setting it to the internal state. The type can be also changed.

Value: React.Component

Render prop version of the useValue() hook. The props are ValueSubscription. The render prop child is (value: string) => ReactNode.

<Value zorm={zo} name={zo.fields.input()}>
    {(value) => <>value</>}
</Value>

parseForm(schema: ZodObject, form: HTMLFormElement | FormData): Type<ZodObject>

Parse HTMLFormElement or FormData with the given Zod schema.

safeParseForm(schema: ZodObject, form: HTMLFormElement | FormData): SafeParseReturnType

Like parseForm() but uses the safeParse() method from Zod.

More Repositories

1

underscore.string

String manipulation helpers for javascript
JavaScript
3,367
star
2

node-hbsfy

Handlebars precompiler plugin for Browserify
JavaScript
257
star
3

immer-reducer

Type-safe and terse reducers with Typescript for React Hooks and Redux
TypeScript
225
star
4

slimux

SLIME inspired tmux integration plugin for Vim
Vim Script
217
star
5

piler

Deprecated Asset Manager for Node.js
CoffeeScript
148
star
6

redux-hooks

βš“ React Hooks implementation for Redux
TypeScript
96
star
7

node-promisepipe

Safely pipe node.js streams while capturing all errors to a single promise
JavaScript
80
star
8

requirejs-hbs

Simple Handlebars loader plugin for RequireJS
JavaScript
78
star
9

lean-redux

Redux state like local component state
JavaScript
78
star
10

angry-caching-proxy

Make package downloads lightning fast!
JavaScript
77
star
11

browserify-externalize

Create external Browserify bundles for lazy asynchronous loading
JavaScript
73
star
12

postcss-ts-classnames

PostCSS plugin to generate TypeScript types from your CSS class names.
TypeScript
70
star
13

jslibs

List of Javascript libraries
59
star
14

backbone.viewmaster

Few tested opinions on how to handle deeply nested views in Backbone.js focusing on modularity.
JavaScript
52
star
15

babel-plugin-ts-optchain

Babel plugin for transpiling legacy browser support to ts-optchain by removing Proxy usage.
TypeScript
30
star
16

trpc-cloudflare-worker

TypeScript
29
star
17

carcounter

Asynchronous module loading example with Browserify
JavaScript
24
star
18

redux-render-prop

Redux with render props. Typescript friendly.
TypeScript
22
star
19

source-map-peek

Peek into original source via source maps from the command line when devtools fail.
JavaScript
21
star
20

TextareaServer

Server for opening external text editors from Chrome
CoffeeScript
19
star
21

deno_karabiner

Write Complex Modifications for Karabiner-Elements using Typescript and Deno.
TypeScript
17
star
22

babel-plugin-display-name-custom

display name inference for your custom react component creators
JavaScript
15
star
23

geekslides

Remote controllable Node.js introduction slides
JavaScript
15
star
24

npm-release

Github action for npm (pre)releases
TypeScript
15
star
25

sauna.reload

Development & Issues MOVED to https://github.com/collective/sauna.reload
Python
13
star
26

TextareaConnect

Edit textareas in Google Chrome using any external editor!
CoffeeScript
13
star
27

node-clim

Console.Log IMproved for Node.js
JavaScript
12
star
28

react-simple

simple style only components for the web & native
JavaScript
9
star
29

typescript-redux-todoapp

Type-safe Boilerplate-free Redux Example
TypeScript
9
star
30

jquery.panfullsize

jQuery plugin for panning large images
JavaScript
8
star
31

neovim-config

Vim Script
8
star
32

Projectwatch

Watch file changes and run multiple css/js pre-processors from one watcher
JavaScript
8
star
33

multip

Tiny multi process init for containers written in Rust πŸ¦€
Rust
8
star
34

ts-box

Put exceptions into boxes πŸ“¦
TypeScript
8
star
35

vimconfig

Vim and other dotfiles
Vim Script
7
star
36

node-tftp

Streaming TFTP Server for node.js
JavaScript
7
star
37

yalr

YALR - Yet Another Live Reload
JavaScript
6
star
38

react-makefile

Makefile boilerplate for React.js
JavaScript
6
star
39

vscode-unsaved

Makes you actually notice when you have unsaved files
TypeScript
5
star
40

reallyexpress

Really Express extends Express with extraordinary features given by Node.js. Work in progress.
CoffeeScript
5
star
41

shell-cheatsheet

4
star
42

tmpshare

Easily share temporary files over http
JavaScript
4
star
43

ircshare

Simple service for sharing your pictures on IRC
JavaScript
3
star
44

qdomain

Promises from domains
JavaScript
3
star
45

ircshare-android

Share pictures easily in IRC from Android phone
Java
3
star
46

browserify-cs-example

Browserify v2 with CoffeeScript Source Maps
CoffeeScript
3
star
47

flysight-subtitles

Convert FlySight data files (.csv) to SubRip (.srt) subtitles.
JavaScript
3
star
48

node-handlebars-runtime

Handlebars runtime only
JavaScript
2
star
49

node-lights

Instanssi Light Server Simulator
CoffeeScript
2
star
50

flips.io

JavaScript
2
star
51

revisioncask

Version control management made simple
Python
2
star
52

subssh

Small framework for creating SSH public key based shell accounts.
Python
2
star
53

toggl-paster

I need to paste Toggl time entries into things so I made a thing.
TypeScript
2
star
54

utils

Epeli's Javascript / Typescript utilities
TypeScript
2
star
55

MAILNETD

Meta Asynchronous Irc Linked Network Email Transport Daemon
Python
1
star
56

rt

Instant tab-complete for npm scripts and Jakefiles
Rust
1
star
57

lunarvim-config

Lua
1
star
58

awesome

My Personal Awesome WM config
Lua
1
star
59

wp-graphql-todoapp

TypeScript
1
star
60

stylify

JavaScript
1
star
61

subssh_buildout

buildout for subssh development purposes
Python
1
star
62

rollsum

Just experiments with rolling checksums. Nothing to see here.
C
1
star
63

sh-thunk

Generate promise returning thunks from shell strings.
JavaScript
1
star
64

LightManager

lightmanager tool written for instanssi.org party
Java
1
star
65

konf

TypeScript
1
star
66

HAL2012

Home hack using Node.js and Raspberry Pi
CoffeeScript
1
star
67

IRCPost

Simple IRC bot with HTTP POST API
CoffeeScript
1
star
68

blog

JavaScript
1
star
69

aswyg-editor

ASWYG Editor - Also See What You Get Editor
JavaScript
1
star