An unopinionated modern form state and data management library
Goodie Forms is an unopinionated modern form state and data management library. It provides a simple and intuitive API for managing form state, validation, and submission. It is designed to be flexible and extensible, allowing you to use it with any UI library or framework.
The project consists of multiple packages, each serving a specific purpose. The core package provides the basic form state management and validation functionalities, while other packages can be used to integrate with specific UI libraries or frameworks.
For detailed documentation, please visit the Goodie Forms Documentation.
- @goodie-forms/core: The core package that provides the headless form state management and manipulation functionalities.
- @goodie-forms/react: A package that provides React hooks for integrating Goodie Forms with React applications.
- @goodie-forms/vue: WIP
import { FormController } from "@goodie-forms/core";
import z from "zod";
// 1. Define a schema for your form data
const userRegisterSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
// 2. Create a form controller with the schema
const formController = new FormController({
validationSchema: userRegisterSchema,
});
// 3. Register form fields
const nameEl = document.getElementById("name") as HTMLInputElement;
const emailEl = document.getElementById("email") as HTMLInputElement;
const passwordEl = document.getElementById("password") as HTMLInputElement;
formController
.registerField(formController.path.of("name"))
.bindElement(nameEl);
formController
.registerField(formController.path.of("email"))
.bindElement(emailEl);
formController
.registerField(formController.path.of("password"))
.bindElement(passwordEl);
// 4. Handle issues
formController.events.on("fieldIssuesUpdated", (path) => {
const field = formController.getField(path)!;
if (field.issues.length !== 0) {
field.boundElement?.classList.add("has-issues");
} else {
field.boundElement?.classList.remove("has-issues");
}
});
// 5. Handle form submission
const formEl = document.getElementById("form") as HTMLFormElement);
const submitHandler = formController.createSubmitHandler(async (data) => {
console.log("Form submitted successfully with data:", data);
});
formEl.onsubmit = submitHandler;import { useForm } from "@goodie-forms/react";
import z from "zod";
// 1. Define a schema for your form data
const userRegisterSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
export function App() {
// 2. Create a form with the schema
const form = useForm(
{
validationSchema: userRegisterSchema,
},
{
validationMode: "onBlur",
revalidationMode: "onChange",
},
);
// 3. Create a submit handler
const handleSubmit = form.createSubmitHandler(async (data) => {
console.log("Form submitted successfully with data:", data);
});
return (
// 4. Bind submit handler to the form element
<form onClick={handleSubmit}>
{/* 5. Render fields */}
<FieldRenderer
form={form}
path={form.path.of("name")}
defaultValue=""
render={({ fieldProps, field, form }) => (
<div>
<input
{...fieldProps}
type="text"
disabled={form.controller.isSubmitting}
/>
{field.issues.length > 0 && (
<div className="issues">
{field.issues.map((issue) => (
<p key={issue.id}>{issue.message}</p>
))}
</div>
)}
</div>
)}
/>
{/* 5. Render fields */}
<FieldRenderer
form={form}
path={form.path.of("email")}
defaultValue=""
render={({ fieldProps, field }) => (
<div>
<input
{...fieldProps}
type="email"
disabled={form.controller.isSubmitting}
/>
{field.issues.length > 0 && (
<div className="issues">
{field.issues.map((issue) => (
<p key={issue.id}>{issue.message}</p>
))}
</div>
)}
</div>
)}
/>
{/* 5. Render fields */}
<FieldRenderer
form={form}
path={form.path.of("password")}
defaultValue=""
render={({ fieldProps, field }) => (
<div>
<input
{...fieldProps}
type="password"
disabled={form.controller.isSubmitting}
/>
{field.issues.length > 0 && (
<div className="issues">
{field.issues.map((issue) => (
<p key={issue.id}>{issue.message}</p>
))}
</div>
)}
</div>
)}
/>
<button type="submit">Submit</button>
</form>
);
}Β© 2026 Taha AnΔ±lcan Metinyurt (iGoodie)
For any part of this work for which the license is applicable, this work is licensed under the Attribution-ShareAlike 4.0 International license. (See LICENSE).
