一覧へ戻る
schalkventer/frrm(新しいタブで開きます)
主要言語: TypeScript
🐇 Tiny 0.5kb Zod-based, HTML form abstraction that goes brr.
- star 数
- 32
- watcher 数
- 1
- fork 数
- 0
- issue 数
- 1
- 最終更新
- 2026/01/15
主要言語: TypeScript
🐇 Tiny 0.5kb Zod-based, HTML form abstraction that goes brr.
Tiny 0.5kb Zod-based, HTML form abstraction that goes brr.
⭐ If you find this tool useful please consider giving it a star on Github ⭐
The latest v2 of frrm uses Zod v4 (npm install frrm). If you want to use frrm with Zod v3 then you need to install version 1.4.2 (npm install frrm@1). Note currently the API is exactly the same, the only difference is the Zod version you plan on using.
import { create, attach } from 'frrm' import { z } from 'zod' const handler = create({ /** * If string value then will replace submit button text with provided value * while server request is resolving. Will also disable all buttons and * inputs. If `true` is passed the label won't be replaced, but everything will * still disable. Alternatively you can pass a callback if you want to * manually handle the busy state (will prevent default behaviour). */ onBusy: "Loading...", /** * Applies client-side Zod validation to determine whether `onSubmit` should * fire. */ schema: z.object({ email: z.string().min(1, { message: "Email value is required" }).email({ message: "Email is not formatted correctly", }), password: z .string() .min(1, { message: "Password value is required", }) .min(6, { message: "Password is required to be at least 6 characters", }), }), /** * Will inject error message into the provided DOM element. Alternatively a * callback can be provided that accepts both `timestamp` and `value` * properties. * * Note that error is automatically removed when the form is * submitted again - likewise a `null` value will be passed to the callback * (if used). */ onError: document.querySelector('[role="alert"]'), onSubmit: (submission) => { /** * Fake server request that takes 4 seconds to resolve, and throws on * incorrect email or password. */ return new Promise((resolve) => { try { setTimeout(() => { if (submission.email !== "john@example.com") resolve(Error("Invalid email")); if (submission.password !== "hunter2") resolve(Error("Invalid password")); resolve(undefined); }, 4000); } catch (error) { console.error(error); resolve(Error("Something went wrong")); } }); }, }); /** * If you are using a framework like React, then you can simply pass the * instance to the onSubmit handler (see example further down page). * However, if you are using plain JavaScript, then you need to * attach the event listener manually. You can use the `attach` * function to do this if you want, since it provides a returned object * with a `remove` method for cleanup. */ attach(document.querySelector("form")!, handler);
@keyframes enter { from { transform: translateY(-0.2rem); opacity: 0.5; } to { transform: translateY(0); opacity: 1; } } form { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; } [role="alert"] > * { background: rgba(255, 0, 0, 0.05); padding: 1rem; animation: enter 0.3s ease; }
<form> <label> <span>Email:</span> <input type="email" name="email" /> </label> <div> <label> <span>Password:</span> <input type="password" name="password" /> </label> </div> <div role="alert" aria-live="assertive"></div> <button type="submit">Login</button> </form>
When using React you can simply pass the handler as is to onSubmit.
import { z } from "zod"; import { useState } from "react"; import { create } from "./react"; export const Example = () => { const [message, setMessage] = useState({ value: null, timestamp: Date.now(), }); return ( <form className="form" onSubmit={create({ schema, onSubmit: fromServer, onError: setMessage, onBusy: "Loading...", schema: z.object({ email: z.string().min(1, { message: "Email value is required" }).email({ message: "Email is not formatted correctly", }), password: z .string() .min(1, { message: "Password value is required", }) .min(6, { message: "Password is required to be at least 6 characters", }), }), })} > <label> <span>Email:</span> <input type="email" name="email" /> </label> <div> <label> <span>Password:</span> <input type="password" name="password" /> </label> </div> <div> {message.value && ( <div className="message" key={`${message.value}-${message.timestamp}`}> {message.value} </div> )} </div> <button type="submit"> Login </button> </form> ); };
Pretty much. Technically it is ~0.537kb . This is the minified code:
const e=e=>{const{onSubmit:t,schema:r,onError:a}=e;return async e=>{e.preventDefault(),a({value:null,timestamp:Date.now()});const n=e.currentTarget,o=Object.fromEntries(new FormData(n));try{const e=r.parse(o),n=await t(e);n&&a({value:n,timestamp:Date.now()})}catch(e){if(e.errors.length)return n.querySelector(`[name="${e.errors[0].path[0]}"]`).focus(),a({value:e.errors[0].message,timestamp:Date.now()});throw e}}},t=(e,t)=>(e.addEventListener("submit",t),{remove:()=>e.removeEventListener("submit",t)});export{t as attach,e as create};