Ozef is an opinionated library that aims to guarantee type-safe, declarative forms with minimal boilerplate. It is built on top of Zod which provides a powerful type system for validating data.
Features Ozef supports:
- Guaranteed type-safety in
onSubmit
- async
onSubmit
- Declarative forms
- Validation checking
- Input types like radio and select
Ozef is not a component library. It does not provide any pre-built components. Instead, plug in existing components to build forms.
Ozef lets you build forms like:
import Input from "./CustomInput";
const NewFlowForm = ozef({
schema: z.object({
name: z.string().min(3),
email: z.string().email(),
// for radios
favoriteColor: z.enum(["Red", "Blue"]),
// for selects
favoriteColor: z.union([z.literal("Red"), z.literal("Blue")]),
}),
// Plug in your own input components
Input,
// Define components for each type
InputRadio: ({ radioValue, ...props }) => (
<div>
{radioValue}
<input type="radio" {...props} />
</div>
),
// Error labels
Error: ({ error }) => <span className="text-red-500">{error}</span>,
Submit: ({ submitting }) => (
<RoundedButton type="submit" loading={submitting}>
Submit
</RoundedButton>
),
});
<NewFlowForm
className="flex flex-col gap-2"
onSubmit={async (vals) => {
/** vals has the type { name: string, email: string, favoriteColor: "Red" | "Blue" } */
...
}}
>
<NewFlowForm.Field.Name prefixIcon="icon_1" />
<NewFlowForm.Error.Name />
<NewFlowForm.Field.Email />
<NewFlowForm.Error.Email />
<NewFlowForm.Error.FavoriteColor />
<NewFlowForm.Field.FavoriteColor>
<NewFlowForm.Field.FavoriteColor.Blue />
<NewFlowForm.Field.FavoriteColor.Red />
</NewFlowForm.Field.FavoriteColor>
<NewFlowForm.Event.Submit />
<NewFlowForm.Error.Submission error="Please fill out the form" />
</NewFlowForm>
with full type-script support!
Ozef has minimal dependencies (just Zod and Jotai) and is easy to install.
npm i zod@3.21.4 jotai ozef
import ozef from "ozef";
const Form = ozef({
schema: z.object({
name: z.string().min(3),
email: z.string().email(),
}),
});
const SomeComponent = () => {
return (
<Form
onSubmit={async (vals) => {
// vals is guaranteed to be of type { name: string, email: string }
...
}}
>
// Use `Field` components to render inputs for the form
<Form.Field.Name />
<Form.Field.Email />
// Use `Error` components to render error labels
<Form.Error.Name />
// Use `Event` components to render special user events components
<Form.Event.Submit />
</Form>
);
};
Components need to modified before being able to be used with Ozef. This is because Ozef needs to be able to pass certain props to the components.
import { type OzefInputProps } from "ozef";
type InputProps = OzefInputProps & {
// Add your own props
prefixIcon?: string;
};
const Input = ({ prefixIcon, hasError, ...props }: InputProps) => {
return (
<div
className={`${
props.className
} ${hasError ? "focus-within:ring-red-500" : ""}`}
>
{prefixIcon && (
<MaterialsIcon className="!text-xl text-zinc-500" icon={prefixIcon} />
)}
<input
{/* This is the important part. Ozef needs to pass props to the native input component. */}
{...props}
className="..."
/>
</div>
);
};
import ozef from "ozef";
const Form = ozef({
schema: z.object({
name: z.string().min(3),
email: z.string().email(),
}),
defaults: {
name: "John Doe",
email: "john-doe@gmail.com"
}
});
const SomeComponent = () => {
return (
<Form
onSubmit={async (vals) => {
// vals is guaranteed to be of type { name: string, email: string }
...
}}
>
// Use `Field` components to render inputs for the form
<Form.Field.Name />
<Form.Field.Email />
// Use `Error` components to render error labels
<Form.Error.Name />
// Use `Event` components to render special user events components
<Form.Event.Submit />
</Form>
);
};
If you're passing down a variable as defaults, you'll need to use useMemo
to prevent the form from re-rendering every time the parent component re-renders.
import ozef from "ozef";
import { useMemo } from "react";
const SomeComponent = ({defaults}: Props) => {
const Form = useMemo(() => ozef({
schema: z.object({
name: z.string().min(3),
email: z.string().email(),
}),
defaults
}), [defaults])
return (
<Form
onSubmit={async (vals) => {
// vals is guaranteed to be of type { name: string, email: string }
...
}}
>
// Use `Field` components to render inputs for the form
<Form.Field.Name />
<Form.Field.Email />
// Use `Error` components to render error labels
<Form.Error.Name />
// Use `Event` components to render special user events components
<Form.Event.Submit />
</Form>
);
};
import ozef from "ozef";
const UpdateRewriteSettingsForm = ozef({
schema: z.object({
terseness: z.enum(["short", "medium", "long"]),
}),
});
const ShadcnExample = () => {
// getter hook
const terseness = UpdateRewriteSettingsForm.Field.Terseness.useValue();
return (
<UpdateRewriteSettingsForm
onSubmit={() => {
// handler
}}
>
<Select
value={terseness}
// setter function
onValueChange={UpdateRewriteSettingsForm.Field.Terseness.setValue}
>
<div className="space-y-1">
<Label>Terseness</Label>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<UpdateRewriteSettingsForm.Field.Terseness.Short />
<UpdateRewriteSettingsForm.Field.Terseness.Medium />
<UpdateRewriteSettingsForm.Field.Terseness.Long />
</SelectContent>
</div>
</Select>
</UpdateRewriteSettingsForm>
);
};