Awesome
<!-- omit in toc -->🐇 frrm
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 ⭐
<img src="https://github.com/user-attachments/assets/7523e907-893a-4540-bc8a-b6800fb8c566" width="500">
Basic Example
JavaScript
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 wont 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. 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);
CSS
@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;
}
HTML
<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>
React Example
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>
);
};
Is it really 0.5kb?
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};