- Introduction
- Features
- Installation
- Basic Usage
- Form Validation with Zod
-
Available Controllers
- Text Input
- Email Input
- Password Input
- Number Input
- Textarea
- Checkbox
- Group Checkbox
- Phone Number Input
- Select (Dropdown)
- Searchable Select
- Multi-Select
- Searchable Multi-Select
- Select with API
- Dependent Select with API
- Date Picker
- Range Date Picker
- Rich Text Editor
- File Upload
- Multiple File Upload
- React Node (Custom Component)
- Conditional Field Display
- Form Groups
- Multi-Step Forms
- API Integration
- Backend Integration
- Custom Form Submission
- UI Customization
- Best Practices
- Troubleshooting
- Conclusion
z-react-dynamic-form
is a powerful, flexible form builder for React applications that enables you to create complex forms with minimal effort. Leveraging Zod for schema validation, the library provides a type-safe approach to form development while supporting a wide range of input types, multi-step forms, and API integrations.
- Type-safe forms with Zod schema validation
- Extensive controller library with 20+ input types
- Multi-step forms with conditional logic
- API integration for dynamic data loading and form submission
- File uploads with preview and validation
- Responsive design with Tailwind CSS support
- Toast notifications for feedback
- Conditional field rendering based on form values
- Seamless backend integration for validation errors
npm install z-react-dynamic-form
Here's a simple example of how to create a form using z-react-dynamic-form
:
import React from "react";
import { DynamicForm } from "z-react-dynamic-form";
import { z } from "zod";
// Define your form schema using Zod
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Please enter a valid email"),
age: z.number().min(18, "You must be at least 18 years old"),
});
// Define your form controllers
const controllers = [
{
name: "name",
label: "Full Name",
type: "text",
placeholder: "Enter your full name",
required: true,
},
{
name: "email",
label: "Email Address",
type: "email",
placeholder: "Enter your email",
required: true,
},
{
name: "age",
label: "Age",
type: "number",
placeholder: "Enter your age",
min: 18,
required: true,
},
];
const MyForm = () => {
const handleSubmit = async ({ values, setError, reset }) => {
try {
console.log("Form submitted with values:", values);
// Submit your form data to an API
// await api.submitForm(values);
reset(); // Reset form after successful submission
} catch (error) {
console.error("Form submission error:", error);
}
};
return (
<DynamicForm
controllers={controllers}
formSchema={formSchema}
handleSubmit={handleSubmit}
/>
);
};
export default MyForm;
The library uses Zod for schema validation, providing type safety and robust validation rules:
import { z } from "zod";
const formSchema = z
.object({
username: z.string().min(3, "Username must be at least 3 characters"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
confirmPassword: z.string(),
terms: z.boolean().refine((val) => val === true, {
message: "You must accept the terms and conditions",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
{
name: "username",
label: "Username",
type: "text",
placeholder: "Enter your username",
required: true,
description: "Choose a unique username for your account",
defaultValue: "",
maximun: 20, // Maximum character length
}
{
name: "email",
label: "Email Address",
type: "email",
placeholder: "user@example.com",
required: true,
}
{
name: "password",
label: "Password",
type: "password",
placeholder: "Enter your password",
required: true,
}
{
name: "age",
label: "Age",
type: "number",
placeholder: "Enter your age",
min: 18,
max: 120,
step: 1,
required: true,
}
{
name: "bio",
label: "Biography",
type: "textarea",
placeholder: "Tell us about yourself",
rows: 4,
description: "Brief description about yourself",
}
{
name: "newsletter",
label: "Subscribe to newsletter",
type: "checkbox",
defaultValue: false,
}
{
name: "interests",
label: "Interests",
type: "group-checkbox",
groupCheckbox: [
{
name: "interests",
label: "Select your interests",
options: [
{ label: "Sports", value: "sports" },
{ label: "Music", value: "music" },
{ label: "Movies", value: "movies" },
{ label: "Reading", value: "reading" },
]
}
]
}
{
name: "phoneNumber",
label: "Phone Number",
type: "phone-number",
placeholder: "Enter your phone number",
defaultValue: {
countryCode: "US",
dialCode: "+1",
phoneNumber: ""
},
}
{
name: "country",
label: "Country",
type: "select",
placeholder: "Select your country",
options: [
{ label: "United States", value: "us" },
{ label: "Canada", value: "ca" },
{ label: "United Kingdom", value: "uk" },
{ label: "Australia", value: "au" },
],
required: true,
}
{
name: "country",
label: "Country",
type: "searchable-select",
placeholder: "Search for a country",
searchPlaceholder: "Type to search...",
minSearchLength: 1,
options: [
{ label: "United States", value: "us" },
{ label: "Canada", value: "ca" },
{ label: "United Kingdom", value: "uk" },
{ label: "Australia", value: "au" },
// Many more options...
],
}
{
name: "skills",
label: "Skills",
type: "multi-select",
placeholder: "Select your skills",
maxSelections: 5, // Maximum number of selections
options: [
{ label: "JavaScript", value: "js" },
{ label: "React", value: "react" },
{ label: "TypeScript", value: "ts" },
{ label: "Node.js", value: "node" },
],
}
{
name: "skills",
label: "Skills",
type: "searchable-multi-select",
placeholder: "Select your skills",
searchPlaceholder: "Search skills",
minSearchLength: 1,
maxSelections: 10,
options: [
// Large list of options...
{ label: "JavaScript", value: "js" },
{ label: "React", value: "react" },
{ label: "TypeScript", value: "ts" },
{ label: "Node.js", value: "node" },
// ...many more
],
}
{
name: "state",
label: "State",
type: "select-from-api",
placeholder: "Select your state",
apiUrl: "https://api.example.com/states",
transformResponse: (data) => {
// Convert API response to options
return data.map(item => ({
label: item.name,
value: item.code
}));
},
}
{
name: "country",
label: "Country",
type: "select-from-api",
placeholder: "Select your country",
apiUrl: "https://api.example.com/countries",
},
{
name: "state",
label: "State",
type: "select-from-api",
placeholder: "Select your state",
apiUrl: "https://api.example.com/states",
optionsApiOptions: {
dependingContrllerName: "country", // Depends on country field
paramName: "countryCode", // Parameter name for the API
},
}
{
name: "birthdate",
label: "Date of Birth",
type: "date",
mode: "single", // Can be "single" or "range"
placeholder: "Select your birth date",
}
{
name: "travelDates",
label: "Travel Dates",
type: "date",
mode: "range",
placeholder: "Select travel dates",
}
{
name: "description",
label: "Project Description",
type: "rich-text-editor",
placeholder: "Describe your project in detail",
}
{
name: "profilePhoto",
label: "Profile Photo",
type: "upload",
multiple: false,
acceptedFileTypes: {
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
},
maxFiles: 1,
}
{
name: "documents",
label: "Documents",
type: "upload",
multiple: true,
acceptedFileTypes: {
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
},
maxFiles: 5,
}
{
name: "custom",
type: "react-node",
reactNode: <div className="p-4 bg-gray-100 rounded-md">
<h3 className="text-lg font-medium">Custom Instructions</h3>
<p>Please read carefully before proceeding.</p>
</div>
}
You can conditionally show/hide fields based on other form values:
{
name: "hasDiscount",
label: "Do you have a discount code?",
type: "checkbox",
},
{
name: "discountCode",
label: "Discount Code",
type: "text",
placeholder: "Enter your discount code",
// Only show this field if hasDiscount is true
visible: (formValues) => formValues.hasDiscount === true,
}
Group related fields together:
{
groupName: "Contact Information",
groupControllers: [
{
name: "email",
label: "Email",
type: "email",
required: true,
},
{
name: "phone",
label: "Phone",
type: "phone-number",
}
]
}
Create multi-step forms with validation at each step:
import React from "react";
import { DynamicForm } from "z-react-dynamic-form";
import { z } from "zod";
// Define schema for each step
const personalInfoSchema = z.object({
firstName: z.string().min(2, "First name is required"),
lastName: z.string().min(2, "Last name is required"),
email: z.string().email("Valid email is required"),
});
const addressSchema = z.object({
address: z.string().min(5, "Address is required"),
city: z.string().min(2, "City is required"),
zipCode: z.string().min(5, "Valid zip code required"),
});
// Define steps
const steps = [
{
stepName: "Personal Information",
stepSchema: personalInfoSchema,
controllers: [
{
name: "firstName",
label: "First Name",
type: "text",
required: true,
},
{
name: "lastName",
label: "Last Name",
type: "text",
required: true,
},
{
name: "email",
label: "Email",
type: "email",
required: true,
},
],
},
{
stepName: "Address",
stepSchema: addressSchema,
controllers: [
{
name: "address",
label: "Street Address",
type: "text",
required: true,
},
{
name: "city",
label: "City",
type: "text",
required: true,
},
{
name: "zipCode",
label: "Zip Code",
type: "text",
required: true,
},
],
},
];
// Combine schemas
const formSchema = z.object({
...personalInfoSchema.shape,
...addressSchema.shape,
});
const StepFormExample = () => {
const handleSubmit = async ({ values }) => {
console.log("Form submitted with values:", values);
// Submit form data
};
return (
<DynamicForm
steps={steps}
formSchema={formSchema}
handleSubmit={handleSubmit}
formtype="steper"
/>
);
};
export default StepFormExample;
The apiOptions
prop accepts an object with the following properties:
type apiOptionsType = {
api: string; // API endpoint URL
method: "POST" | "PATCH" | "PUT" | "DELETE" | "GET"; // HTTP method
options?: AxiosRequestConfig; // Additional Axios request configuration
errorHandler?: (data: any, type: errorHandlertType) => void; // Custom error handler
onFinish?: (data: any) => void; // Callback function after successful submission
};
When you provide apiOptions
without a custom handleSubmit
function, the form automatically handles submission to your API:
<DynamicForm
controllers={controllers}
formSchema={formSchema}
apiOptions={{
api: "https://api.example.com/submit",
method: "POST",
options: {
headers: {
// Additional headers
},
},
onFinish: (responseData) => {
// Handle successful response
console.log("Success:", responseData);
},
}}
/>
With this configuration, the form will:
- Validate all inputs using your Zod schema
- Automatically submit the form data to the specified API endpoint
- Handle loading states during submission
- Display appropriate error messages on failure
- Execute the
onFinish
callback on success
The apiOptions
provides a robust error handling system through the errorHandler
property:
type errorHandlertType = "form" | "modal" | "toast" | "redirect";
<DynamicForm
controllers={controllers}
formSchema={formSchema}
apiOptions={{
api: "https://api.example.com/submit",
method: "POST",
errorHandler: (data, type) => {
if (type === "form") {
// Handle form validation errors returned from API
console.log("Form errors:", data);
} else if (type === "toast") {
// Handle errors to be displayed as toasts
console.log("Toast error:", data);
} else if (type === "modal") {
// Handle errors to be displayed in a modal
console.log("Modal error:", data);
} else if (type === "redirect") {
// Handle redirect responses
window.location.href = data.redirectUrl;
}
},
}}
/>
The library also handles specific API response formats:
When your API returns a success response with an action type, the library can perform specific actions:
{
"status": "success",
"action": "VERIFIED",
"data": {
"user": {
"id": "123",
"email": "user@example.com"
}
}
}
In the example above, if the action
is VERIFIED
, the library can store verification data in localStorage and handle verification flows automatically.
For form validation errors, your API can return:
{
"status": "error",
"action": "form",
"error": [
{
"path": ["email"],
"message": "Email already exists"
},
{
"path": ["password"],
"message": "Password is too weak"
}
]
}
The library will automatically map these errors to the corresponding form fields.
The library also supports One-Time Password (OTP) verification flows. When your API returns a response with:
{
"action": "VERIFIED",
"data": {
// Verification data
}
}
The component switches to the OttpInputHandler
which provides a dedicated interface for entering verification codes. This feature is useful for:
- Email verification
- Phone verification
- Two-factor authentication
- Identity verification processes
The verification data is stored in localStorage under the key defined in the component (VERIFICATION_DATA_LOCASTORAGE_NAME
).
For global API configuration, you can use the initConfig
utility:
import { initConfig } from "z-react-dynamic-form";
initConfig(
{
api: {
baseURL: "https://api.example.com",
headers: {
"Content-Type": "application/json",
"X-API-Key": "your-api-key",
},
timeout: 30000, // 30 seconds
},
},
// Optional token provider function
async () => {
const token = localStorage.getItem("auth_token");
return { accessToken: token };
}
);
This configuration sets up:
- Default base URL for all API requests
- Default headers and timeout
- Authentication token provider that's called automatically before requests
Many of the select controllers (select-from-api
, searchable-select-from-api
, etc.) also support API integration through the apiUrl
and optionsApiOptions
properties:
{
name: "country",
label: "Country",
type: "select-from-api",
placeholder: "Select country",
apiUrl: "https://api.example.com/countries",
transformResponse: (data) => {
// Transform API response to options format
return data.map(item => ({
label: item.name,
value: item.id
}));
}
}
The optionsApiOptions
property provides additional configuration:
{
name: "city",
label: "City",
type: "select-from-api",
placeholder: "Select city",
apiUrl: "https://api.example.com/cities",
optionsApiOptions: {
dependingContrllerName: "country", // Field this depends on
paramName: "countryId", // Parameter name to send to API
includeAll: false, // Whether to include "All" option
params: {
// Additional parameters
limit: 50
}
}
}
With this configuration, when the value of the country
field changes, the component will automatically fetch new options for the city
select field, passing the country value as a parameter.
The library is designed to work seamlessly with backend validation. When your backend detects validation errors, it can send them in a standardized format that the form automatically interprets and displays.
For backend validation errors to be properly mapped to form fields, your API should return the following structure:
{
"error": {
"path": ["fieldName"],
"message": "Error message for this field"
},
"action": "form"
}
The path
array contains the names of the form fields that have errors, and the message
is the error text to display. The action
property with the value "form"
tells the component to treat this as a form validation error.
For a simple form validation error, your backend should return:
{
"error": {
"path": ["email"],
"message": "This email is already registered"
},
"action": "form"
}
This will display the error message under the email field in the form.
To return errors for multiple fields, use an array of error objects:
{
"error": [
{
"path": ["email"],
"message": "This email is already registered"
},
{
"path": ["password"],
"message": "Password must contain at least one uppercase letter"
}
],
"action": "form"
}
This format directly maps to Zod's validation error structure, making it easy to integrate with backend validation libraries that use Zod or similar validation libraries.
For errors that should be displayed in a modal:
{
"data": [
{
"message": "Your session has expired"
},
{
"message": "Please log in again"
}
],
"action": "modal"
}
The component will display these messages in a modal dialog if you provide a modalComponenet
prop.
For errors that should be displayed as toast notifications:
{
"message": "Server is currently undergoing maintenance",
"action": "toast"
}
The component will display this message as a toast notification.
For responses that should trigger a redirect:
{
"redirectUrl": "/login",
"action": "redirect"
}
Your error handler can use this to navigate the user to another page.
Handle form submission with custom logic:
const handleSubmit = async ({ values, setError, reset }) => {
try {
// Show loading state
setSubmitLoading(true);
// Send data to API
const response = await fetch("https://api.example.com/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
// Handle API errors
const errorData = await response.json();
if (errorData.error && errorData.action === "form") {
// Set field-specific errors from backend
if (Array.isArray(errorData.error)) {
errorData.error.forEach((err) => {
setError(err.path[0], {
type: "manual",
message: err.message,
});
});
} else {
setError(errorData.error.path[0], {
type: "manual",
message: errorData.error.message,
});
}
throw new Error("Please correct the form errors");
}
throw new Error(errorData.message || "Form submission failed");
}
// Handle success
toast.success("Form submitted successfully!");
reset(); // Reset form
} catch (error) {
toast.error(error.message);
console.error("Form submission error:", error);
} finally {
setSubmitLoading(false);
}
};
When using apiOptions
, you can provide a custom modal component for displaying API errors:
<DynamicForm
controllers={controllers}
formSchema={formSchema}
apiOptions={{
api: "https://api.example.com/submit",
method: "POST",
}}
modalComponenet={(modal, setModal) => (
<div
className={`fixed inset-0 flex items-center justify-center z-50 ${
modal.open ? "block" : "hidden"
}`}
>
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h3 className="text-lg font-bold mb-4">Error</h3>
<div className="mb-4">
{/* Display modal.data here */}
{modal.data.map((error, index) => (
<p key={index} className="text-red-500">
{error.message}
</p>
))}
</div>
<button
onClick={() => setModal({ ...modal, open: false })}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Close
</button>
</div>
</div>
)}
/>
This modal will be displayed when the API returns an error with the type "modal".
You can customize the submit button appearance and behavior:
<DynamicForm
controllers={controllers}
formSchema={formSchema}
apiOptions={{
api: "https://api.example.com/submit",
method: "POST",
}}
submitBtn={{
label: "Save Changes", // Custom button text
className: "bg-green-600 hover:bg-green-700", // Additional CSS classes
type: "submit", // Button type
disabled: false, // Control disabled state
}}
/>
For complete control over the submission UI, you can use the tricker
prop:
<DynamicForm
controllers={controllers}
formSchema={formSchema}
apiOptions={{
api: "https://api.example.com/submit",
method: "POST",
}}
tricker={({ submitLoading, isValid }) => (
<div className="flex justify-between items-center mt-4">
<button
type="button"
onClick={() => console.log("Cancel")}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
type="submit"
disabled={submitLoading || !isValid}
className={`px-6 py-2 rounded ${
submitLoading || !isValid
? "bg-gray-300 cursor-not-allowed"
: "bg-blue-600 hover:bg-blue-700 text-white"
}`}
>
{submitLoading ? (
<span className="flex items-center">
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Submitting...
</span>
) : (
"Submit Form"
)}
</button>
</div>
)}
/>
Customize form appearance:
<DynamicForm
controllers={controllers}
formSchema={formSchema}
handleSubmit={handleSubmit}
props={{
form: {
className: "space-y-6 p-6 bg-gray-50 rounded-lg shadow-sm",
},
controllerBase: {
className: "grid gap-6 md:grid-cols-2",
},
submitBtn: {
className:
"w-full py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors",
label: "Submit Application",
},
}}
/>
-
Schema Validation: Always define a Zod schema that matches your form structure for type safety and validation.
-
Error Handling: Provide informative error messages in your schema for better user experience.
-
Conditional Fields: Use the
visible
property to conditionally show/hide fields based on form values. -
Field Dependencies: Utilize
optionsApiOptions
withdependingContrllerName
for fields that depend on other field values. -
Form Groups: Group related fields together using
groupControllers
for better organization. -
Responsive Design: Use the
props
object to apply responsive grid layouts. -
Loading States: Handle loading states during form submission to provide feedback to users.
-
Validation Feedback: Use toast notifications or inline errors to provide feedback on validation failures.
-
Default Values: Set appropriate default values for your fields to pre-fill the form.
-
API Error Format: When developing your backend, follow the specified error response format to ensure seamless integration with the form.
-
Form validation not working:
- Ensure your Zod schema correctly matches your form structure
- Check for typos in field names
-
API select not loading options:
- Verify API URL is correct
- Check if
transformResponse
function is properly formatting the data - Confirm that API response format matches expected structure
-
File uploads not working:
- Verify file size is within limits
- Check accepted file types
- Ensure maxFiles is set correctly
-
Dependent fields not updating:
- Confirm
dependingContrllerName
matches exactly with the field name it depends on - Ensure the parent field is properly setting its value
- Confirm
-
Backend validation errors not showing:
- Ensure your API returns errors in the correct format (
{ error: { path: [...], message: "..." }, action: "form" }
) - Check that field names in error paths match your controller names exactly
- Ensure your API returns errors in the correct format (
z-react-dynamic-form
provides a powerful, flexible solution for creating forms in React applications. By combining type safety with Zod, extensive controller options, and seamless backend integration, it simplifies the process of building complex forms while maintaining a great user experience.
The library is designed to handle a wide range of form scenarios, from simple contact forms to complex multi-step registration flows, making it suitable for various application needs.