zodsform
v0.2.4
Published
Generador de formularios dinámicos con validación automática usando Zod
Maintainers
Readme
ZodsForm 🚀
TypeScript library to create dynamic forms with automatic validation using Zod
ZodsForm allows you to create complete HTML forms from Zod schemas or JSON structures, with real-time validation, and a simple, intuitive API.

✨ Features
- 🎯 Auto-generation from Zod: Convert Zod schemas into functional forms
- ✅ Real-time validation: Validation while user types and on blur
- 🔄 Nested field support: Objects and complex structures with dot-notation
- � Array support: Dynamic arrays with add/remove controls and element-level validation
- �📦 Native TypeScript: Full typing and autocomplete
- 🎭 Input masks: Automatic formatting (phone, credit card, ZIP, etc.) with string or regex
- 🔐 Password fields: With automatic show/hide toggle
- 🎨 Custom components: 3-level system (global, instance, field)
- 🎛️ Dynamic properties: Change visibility, size, options in real-time
- 🌐 Multiple input types: text, number, email, password, textarea, select, checkbox
- 🎯 Auto-mounting: Specify the element where the form will be mounted
- 🔍 Cross-field validation: Support for Zod's
.refine()(e.g., confirm password)
📦 Installation
npm install zodsform zodOr with yarn:
yarn add zodsform zodNote: ZodsForm uses pure CSS (no CSS frameworks required). Just import the styles:
import 'zodsform/dist/styles.css'Or in HTML:
<link rel="stylesheet" href="node_modules/zodsform/dist/styles.css">No additional dependencies or build tools needed! The CSS file is ready to use out of the box.
🚀 Quick Start
1. From a Zod Schema with Extensions (Recommended)
import { ZodsForm, z } from "zodsform";
import "zodsform/dist/styles.css";
// Define your Zod schema with ZodsForm extensions
const userSchema = z.object({
firstName: z.string()
.min(2, "Minimum 2 characters")
.label("First Name") // 🏷️ Custom label
.properties({ size: 6 }), // 📏 Grid size (1-12)
lastName: z.string()
.min(2, "Minimum 2 characters")
.label("Last Name")
.properties({ size: 6 }),
email: z.string()
.email("Invalid email")
.label("Email Address"),
age: z.number()
.min(18, "Must be 18 or older")
.label("Age")
.properties({ visible: false }), // 👁️ Hidden by default
});
// Create the form automatically with element selector
const form = ZodsForm.fromSchema(userSchema, {
el: "#root", // CSS selector or DOM element where it will be mounted
onSubmit: ({ data }) => {
console.log("Validated data:", data);
// Send to your API
},
onChange: ({ fieldPath, data, errors, arrayIndex }) => {
// 🎯 fieldPath has strong typing based on schema!
// Autocomplete: "email" | "password" | "age" | "security.password"
// arrayIndex indicates which array element changed (if applicable)
console.log("Field changed:", fieldPath);
console.log("Array index:", arrayIndex); // undefined for non-array fields
console.log("Current data:", data);
console.log("Errors:", errors);
},
onValidate: ({ isValid, data, errors }) => {
console.log("Form valid:", isValid);
}
});
// Automatically mounted on the specified element
form.render();
// Or you can mount it manually without specifying 'el':
// document.getElementById("root")?.appendChild(form.render());2. With Input Masks
const contactSchema = z.object({
phone: z.string()
.length(10, "Phone must have 10 digits")
.label("Phone")
.properties({ mask: "###-###-####" }), // 📞 Format mask
zipCode: z.string()
.length(5, "Invalid ZIP code")
.label("ZIP Code")
.properties({ mask: "#####" }),
creditCard: z.string()
.length(16, "Invalid card")
.label("Credit Card")
.properties({ mask: "####-####-####-####" }),
customMask: z.string()
.label("Only numbers 1-6")
.properties({ mask: /^[1-6]\d{0,5}$/ }), // 🎭 Regex mask
});3. With Password Fields
const securitySchema = z.object({
security: z.object({
password: z.string()
.min(8, "Minimum 8 characters")
.label("Password")
.password(), // 🔐 Automatic show/hide toggle
confirmPassword: z.string()
.label("Confirm Password")
.password(),
}).label("Security"),
})
.refine((data) => data.security.password === data.security.confirmPassword, {
message: "Passwords do not match",
path: ["security.confirmPassword"], // ✅ Cross-field validation
});📚 Zod Extensions
ZodsForm extends Zod with chainable methods to configure fields:
| Method | Description | Example |
|--------|-------------|---------|
| .label(string) | Defines the label text | z.string().label("Full Name") |
| .properties({...}) | Configures field properties | z.string().properties({ size: 6, mask: "###-####" }) |
| .password(boolean) | Converts to password field with toggle | z.string().password() |
| .component(ComponentConfig) | Assigns a custom component | z.boolean().component(ToggleSwitch) |
Available Properties
.properties({
size?: number; // Grid size (1-12 columns)
visible?: boolean; // Show/hide field
disabled?: boolean; // Enable/disable
placeholder?: string; // Placeholder text
mask?: string | RegExp; // Input mask
// For select:
options?: Array<{
value: string | number;
label: string;
}>;
})📚 Supported Field Types
| Type | Description | Zod Example |
|------|-------------|-------------|
| text | Text input | z.string() |
| number | Numeric input | z.number() |
| email | Email input | z.string().email() |
| password | Password input with toggle | z.string().password() |
| textarea | Text area | z.string() (long) |
| select | Dropdown selector | z.enum(["a", "b"]) or z.enum({ a: "A", b: "B" }) |
| checkbox | Checkbox | z.boolean() |
| array | Dynamic array with controls | z.array(z.object({...})) |
| section | Visual container | - |
| box | Bordered container | z.object({...}).label("Title") |
🔥 Advanced Examples
Form with Nested Objects
const addressSchema = z.object({
user: z.object({
name: z.string().label("Name"),
email: z.string().email().label("Email"),
}).label("User"), // 📦 Box with title
address: z.object({
street: z.string().label("Street"),
city: z.string().label("City"),
zipCode: z.string()
.length(5)
.label("ZIP Code")
.properties({ mask: "#####" }),
}).label("Address"), // 📦 Another box
});
const form = ZodsForm.fromSchema(addressSchema, {
onSubmit: (data) => {
console.log(data);
// {
// user: { name: "...", email: "..." },
// address: { street: "...", city: "...", zipCode: "12345" }
// }
},
});Form with Select (Enum)
// Simple enum (uses values as labels)
const simpleEnum = z.object({
role: z.enum(["admin", "user", "guest"]).label("Role"),
});
// Enum with custom labels (recommended)
const customEnum = z.object({
country: z.enum({
mx: "Mexico",
us: "United States",
es: "Spain",
ar: "Argentina"
}).label("Country"),
});
const form = ZodsForm.fromSchema(customEnum);Form with Custom Validations
const schema = z.object({
username: z
.string()
.min(3, "Minimum 3 characters")
.max(20, "Maximum 20 characters")
.regex(/^[a-zA-Z0-9_]+$/, "Only letters, numbers and underscore"),
age: z
.number()
.min(18, "Must be 18 or older")
.max(100, "Invalid age"),
website: z
.string()
.url("Invalid URL")
.optional(),
});Form with Arrays
const securitySchema = z.object({
security: z.array(
z.object({
password: z.string()
.min(8, "Minimum 8 characters")
.label("Password")
.password(),
confirmPassword: z.string()
.label("Confirm Password")
.password(),
})
).label("Security Credentials")
.min(1, "At least one set of credentials is required"),
})
.refine((data) => {
// Validate that passwords match in all array elements
return data.security.every(item => item.password === item.confirmPassword);
}, {
message: "Passwords do not match",
path: ["security", 0, "confirmPassword"], // Error shown on first element
});
const form = ZodsForm.fromSchema(securitySchema, {
onChange: ({ fieldPath, data, arrayIndex }) => {
// arrayIndex indicates which array element changed
if (arrayIndex !== undefined) {
console.log(`Field ${fieldPath} changed in array element ${arrayIndex}`);
}
},
onSubmit: ({ data }) => {
console.log(data);
// {
// security: [
// { password: "...", confirmPassword: "..." },
// { password: "...", confirmPassword: "..." }
// ]
// }
}
});Array Features:
- ➕ Add button: "+ Agregar" button to add new elements
- ✕ Remove button: Each element has a remove button
- 🛡️ Smart deletion: If only one element remains, it clears the data instead of removing the element
- ✅ Independent validation: Each array element validates independently
- 🎯 Element tracking:
arrayIndexparameter inonChangecallback to identify which element changed - 🔍 Cross-field validation:
refine()works correctly with array elements
🎛️ API
ZodsForm.fromSchema(schema, config)
Creates a form from a Zod schema.
Parameters:
schema: Zod schema (ZodObject)config:el?: CSS selector (string) or DOM element where to mount the formonValidate?: Callback when the form is validated({ isValid, data, errors }) => voidonSubmit?: Callback when the form is submitted({ data }) => voidonChange?: Callback on each field change({ fieldPath, data, errors, arrayIndex }) => voidfieldPathhas strong typing based on schema structure (includes array element properties!)arrayIndex?is a number indicating which array element changed (undefined for non-array fields)
components?: Custom components map
Returns: ZodsForm instance
Example:
const form = ZodsForm.fromSchema(mySchema, {
el: "#app", // Automatically mounted on this element
onSubmit: ({ data }) => console.log(data),
onChange: ({ fieldPath, data, errors, arrayIndex }) => {
// fieldPath has autocomplete: "email" | "password" | "nested.field" | "array.property"
console.log('Changed:', fieldPath);
if (arrayIndex !== undefined) {
console.log('Array element index:', arrayIndex);
}
}
});
form.render();new ZodsForm(config, el?)
Creates a form from a JSON structure.
Parameters:
config.structure: Form structureconfig.schema?: Optional Zod schema for full validationconfig.onSubmit?: Submit callback({ data }) => voidconfig.onChange?: Change callback({ fieldPath, data, errors, arrayIndex }) => voidconfig.onValidate?: Validate callback({ isValid, data, errors }) => voidconfig.components?: Custom components mapel?: CSS selector or DOM element (optional)
Instance Methods
form.render(): HTMLElement
Renders the form and returns the DOM element.
form.getData(): any
Gets the current form data.
form.getErrors(): any
Gets the current validation errors.
form.setData(data: Record<string, any>): void
Sets values in the form programmatically.
form.setData({
firstName: "John",
email: "[email protected]",
age: 25,
});form.setFieldProperty(fieldPath, property, value): void
Dynamically changes any field property in the form in real-time. Supports nested fields with dot-notation.
// Hide/show fields
form.setFieldProperty("companyName", "visible", false);
form.setFieldProperty("address.street", "visible", true); // ✅ Nested fields
// Change grid size (1-12 columns)
form.setFieldProperty("email", "size", 12);
// Enable/disable fields
form.setFieldProperty("zipCode", "disabled", true);
// Update select options
form.setFieldProperty("country", "options", [
{ value: "mx", label: "Mexico" },
{ value: "us", label: "USA" }
]);
// Change min/max limits
form.setFieldProperty("age", "min", 18);
// Apply custom CSS classes
form.setFieldProperty("email", "className", "bg-yellow-100");
// Change masks (string or regex)
form.setFieldProperty("phone", "mask", "###-###-####");Supported properties: visible, size, disabled, className, min, max, options, mask
🎭 Mask System
ZodsForm supports two types of masks for data input:
1. Format Masks (String)
Apply automatic formatting while the user types. The # symbol represents a digit:
const maskedSchema = z.object({
phone: z.string().properties({ mask: "###-###-####" }), // 555-123-4567
zipCode: z.string().properties({ mask: "#####" }), // 12345
creditCard: z.string().properties({ mask: "####-####-####-####" }), // 1234-5678-9012-3456
});Important: The value is stored WITHOUT formatting (digits only), but displayed formatted in the input.
2. Validation Masks (RegExp)
Validate each character in real-time according to a regular expression:
const regexMaskSchema = z.object({
customCode: z.string().properties({
mask: /^[1-6]\d{0,5}$/ // Only numbers 1-6, maximum 6 digits
}),
alphanumeric: z.string().properties({
mask: /^[a-zA-Z0-9]*$/ // Only letters and numbers
}),
});🔧 Grid System
ZodsForm uses a 12-column grid system based on CSS Grid. Use the size property to control width:
const gridSchema = z.object({
firstName: z.string()
.label("First Name")
.properties({ size: 6 }), // 50% width (6/12)
lastName: z.string()
.label("Last Name")
.properties({ size: 6 }), // 50% width (6/12)
email: z.string()
.label("Email")
.properties({ size: 12 }), // 100% width (12/12)
});Common sizes:
size: 3→ 25% (3/12)size: 4→ 33% (4/12)size: 6→ 50% (6/12)size: 8→ 66% (8/12)size: 12→ 100% (12/12) - default for boxes and textareas
🎨 Custom Components
ZodsForm allows you to completely customize the appearance of your forms with three levels of customization in priority order:
1. Field-level Components (Zod) - Highest Priority
const schema = z.object({
notifications: z.boolean()
.label("Notifications")
.component(ToggleSwitch), // 🎯 Only this field uses ToggleSwitch
});2. Instance-level Components
const form = ZodsForm.fromSchema(schema, {
components: {
boolean: MyCustomToggle, // By type
acceptTerms: StyledCheckbox, // By field name
},
onSubmit: (data) => console.log(data)
});3. Global Components
import { ZodsForm } from "zodsform";
// In your main.ts or index.ts
ZodsForm.registerComponents({
boolean: ToggleSwitch, // All booleans use toggle
});Creating Custom Components
import type { ComponentConfig } from "zodsform";
export const ToggleSwitch: ComponentConfig = {
render: (config) => {
const wrapper = document.createElement('div');
const input = document.createElement('input');
input.type = 'checkbox';
input.name = config.fieldPath;
// ... your custom UI logic
wrapper.appendChild(input);
return wrapper;
},
getValue: (element) => {
return element.querySelector('input')?.checked || false;
},
setValue: (element, value) => {
const input = element.querySelector('input');
if (input) input.checked = Boolean(value);
}
};🤝 Contributing
Contributions are welcome! Please:
- Fork the project
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
🔄 Migration from v0.1.x
If you were using previous versions, here are the main changes:
- ✅ Nested fields fixed: Validation of fields within objects now works correctly
- ✅ Improved masks: Support for regex in addition to string patterns
- ✅ Improved components: Clearer 3-level priority system
- ⚠️ Removed methods:
labelsin config no longer exists, use.label()in schema
❓ FAQ
Los estilos no se muestran después de instalar el paquete
Solución: Asegúrate de importar el CSS en tu proyecto:
import 'zodsform/dist/styles.css';O en HTML:
<link rel="stylesheet" href="node_modules/zodsform/dist/styles.css">El CSS está listo para usar sin necesidad de herramientas de build adicionales.
¿Necesito instalar algún framework CSS?
No. ZodsForm usa CSS puro sin dependencias externas. Todo está incluido en el archivo dist/styles.css. Solo necesitas instalar zodsform y zod.
¿Puedo personalizar los estilos?
Sí, de tres formas:
- CSS personalizado: Sobreescribe las clases de ZodsForm en tu propio CSS
- Componentes personalizados: Crea tus propios componentes con tu lógica de UI
- Propiedades inline: Usa
classNameen.properties()para agregar clases personalizadas
� Additional Documentation
- QUICKSTART.md: 5-minute quick guide
- CHANGELOG.md: Change history
- PUBLISHING.md: Publishing guide for maintainers
- Demos: See
src/demos.htmlfor interactive examples
👤 Author
Dinnger
🙏 Acknowledgments
- Zod - TypeScript-first validation library
- The TypeScript community
📝 License
ISC License - see the LICENSE file for more details
Made with ❤️ by Dinnger
