Package Exports
- z-react-dynamic-form
- z-react-dynamic-form/dist/index.esm.js
- z-react-dynamic-form/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (z-react-dynamic-form) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
z-react-dynamic-form Documentation
Table of Contents
- 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
Introduction
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.
Features
- 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
Installation
npm install z-react-dynamic-form
Basic Usage
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;
Form Validation with Zod
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"],
});
Available Controllers
Text Input
{
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
}
Email Input
{
name: "email",
label: "Email Address",
type: "email",
placeholder: "user@example.com",
required: true,
}
Password Input
{
name: "password",
label: "Password",
type: "password",
placeholder: "Enter your password",
required: true,
}
Number Input
{
name: "age",
label: "Age",
type: "number",
placeholder: "Enter your age",
min: 18,
max: 120,
step: 1,
required: true,
}
Textarea
{
name: "bio",
label: "Biography",
type: "textarea",
placeholder: "Tell us about yourself",
rows: 4,
description: "Brief description about yourself",
}
Checkbox
{
name: "newsletter",
label: "Subscribe to newsletter",
type: "checkbox",
defaultValue: false,
}
Group Checkbox
{
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" },
]
}
]
}
Phone Number Input
{
name: "phoneNumber",
label: "Phone Number",
type: "phone-number",
placeholder: "Enter your phone number",
defaultValue: {
countryCode: "US",
dialCode: "+1",
phoneNumber: ""
},
}
Select (Dropdown)
{
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,
}
Searchable Select
{
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...
],
}
Multi-Select
{
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" },
],
}
Searchable Multi-Select
{
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
],
}
Select with API
{
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
}));
},
}
Dependent Select with API
{
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
},
}
Date Picker
{
name: "birthdate",
label: "Date of Birth",
type: "date",
mode: "single", // Can be "single" or "range"
placeholder: "Select your birth date",
}
Range Date Picker
{
name: "travelDates",
label: "Travel Dates",
type: "date",
mode: "range",
placeholder: "Select travel dates",
}
Rich Text Editor
{
name: "description",
label: "Project Description",
type: "rich-text-editor",
placeholder: "Describe your project in detail",
}
File Upload
{
name: "profilePhoto",
label: "Profile Photo",
type: "upload",
multiple: false,
acceptedFileTypes: {
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
},
maxFiles: 1,
}
Multiple File Upload
{
name: "documents",
label: "Documents",
type: "upload",
multiple: true,
acceptedFileTypes: {
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
},
maxFiles: 5,
}
React Node (Custom Component)
{
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>
}
Conditional Field Display
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,
}
Form Groups
Group related fields together:
{
groupName: "Contact Information",
groupControllers: [
{
name: "email",
label: "Email",
type: "email",
required: true,
},
{
name: "phone",
label: "Phone",
type: "phone-number",
}
]
}
Multi-Step Forms
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;
API Integration
API Options Configuration
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
};
Automatic Form 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
Error Handling
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;
}
},
}}
/>
API Response Handling
The library also handles specific API response formats:
Success Type Response
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.
Form Error Response
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.
Verification Flow with OTTP
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
).
Global API Configuration
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
API Options for Select Controllers
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.
Backend Integration
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.
Error Response Format
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.
Validation Error Response
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.
Multiple Error Fields
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.
Modal Error Response
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.
Toast Error Response
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.
Redirect Response
For responses that should trigger a redirect:
{
"redirectUrl": "/login",
"action": "redirect"
}
Your error handler can use this to navigate the user to another page.
Custom Form Submission
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);
}
};
UI Customization
Custom Modal Component
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".
Submit Button Customization
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
}}
/>
Custom Submission Trigger
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>
)}
/>
Form Styling
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",
},
}}
/>
Best Practices
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.
Troubleshooting
Common Issues
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 (
Conclusion
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.