React Form with Dynamic Validation
This challenge involves building a reusable form component in React with TypeScript that supports various input types and dynamic validation rules. A well-designed form component is fundamental for user interaction in web applications, allowing for data collection and ensuring data integrity before submission.
Problem Description
You are tasked with creating a generic Form component in React using TypeScript. This component should be able to render a form with different input fields, each with its own validation rules.
Key Requirements:
- Generic Form Component: Create a
Formcomponent that accepts an array of field configurations. Each field configuration should define the input type, name, label, initial value, and validation rules. - Input Field Types: Support at least the following input types:
textemailpasswordnumber
- Validation Rules: Each field should allow for multiple validation rules. Support the following validation rule types:
required: Checks if the field has a value.minLength: Checks if the field's value meets a minimum length.maxLength: Checks if the field's value does not exceed a maximum length.pattern: Checks if the field's value matches a given regular expression.custom: Allows for a custom validation function that returns an error message string ornull.
- Error Display: Validation errors should be displayed clearly below the corresponding input field.
- Form Submission: The form should have a submit button. When the submit button is clicked, the form should validate all fields. If all fields are valid, a callback function (
onSubmit) provided to theFormcomponent should be called with the current form values. If any field is invalid, errors should be displayed, and theonSubmitcallback should not be triggered. - State Management: Manage the state of form values and validation errors within the
Formcomponent. - Reusability: The
Formcomponent should be highly reusable, allowing different forms with different structures and validation to be rendered by passing different field configurations.
Expected Behavior:
- On initial render, the form should display input fields with their labels. No errors should be shown initially.
- As the user types into an input field, the validation for that specific field should occur (consider debouncing if performance is a concern, though not strictly required for this challenge).
- If a field fails validation, an appropriate error message should appear beneath it.
- If a field passes validation, any previously displayed error message should disappear.
- Clicking the submit button should trigger validation for all fields.
- If all fields are valid upon submission, the
onSubmitfunction will be called with an object containing the latest form values. - If any field is invalid upon submission, the form will remain, errors will be displayed, and
onSubmitwill not be called.
Edge Cases:
- Handling empty or null initial values.
- The
patternvalidation rule with various regex expressions. - The
customvalidation rule with complex logic.
Examples
Example 1: Simple Text Input with Required and Min Length
Let's imagine a UserProfileForm component that uses our Form component.
Input (Field Configuration):
const userProfileFields = [
{
name: 'username',
label: 'Username',
type: 'text',
initialValue: '',
validationRules: [
{ type: 'required', message: 'Username is required.' },
{ type: 'minLength', value: 5, message: 'Username must be at least 5 characters long.' },
],
},
{
name: 'bio',
label: 'Biography',
type: 'text',
initialValue: '',
validationRules: [
{ type: 'maxLength', value: 200, message: 'Biography cannot exceed 200 characters.' },
],
},
];
// When the form is submitted with valid data:
// onSubmit({ username: 'Alice', bio: 'A passionate developer.' })
Expected Behavior:
- If the user submits with
usernameempty, an error "Username is required." appears. - If the user submits with
usernameas "Al", an error "Username must be at least 5 characters long." appears. - If the user submits with
usernameas "Alice" andbioas a string longer than 200 characters, an error "Biography cannot exceed 200 characters." appears below the bio field.
Example 2: Email Input with Required and Pattern Validation
Input (Field Configuration):
const contactFormFields = [
{
name: 'email',
label: 'Email Address',
type: 'email', // Special type that often implies pattern validation internally, but we'll define explicitly
initialValue: '',
validationRules: [
{ type: 'required', message: 'Email is required.' },
{ type: 'pattern', value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address format.' },
],
},
];
// When the form is submitted with valid data:
// onSubmit({ email: 'test@example.com' })
Expected Behavior:
- If the user submits with
emailempty, "Email is required." appears. - If the user submits with
emailas "test@", "Invalid email address format." appears. - If the user submits with
emailas "test@example.com", theonSubmitcallback is invoked.
Example 3: Custom Validation and Mixed Types
Input (Field Configuration):
const signupFormFields = [
{
name: 'age',
label: 'Age',
type: 'number',
initialValue: '',
validationRules: [
{ type: 'required', message: 'Age is required.' },
{ type: 'custom', validator: (value: string) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && numValue < 18) {
return 'You must be at least 18 years old.';
}
return null;
},
message: 'Must be 18 or older.' // This message might be redundant if custom validator returns specific msg
},
],
},
{
name: 'password',
label: 'Password',
type: 'password',
initialValue: '',
validationRules: [
{ type: 'required', message: 'Password is required.' },
{ type: 'minLength', value: 8, message: 'Password must be at least 8 characters long.' },
],
},
{
name: 'confirmPassword',
label: 'Confirm Password',
type: 'password',
initialValue: '',
validationRules: [
{ type: 'required', message: 'Confirm password is required.' },
{ type: 'custom', validator: (value: string, allValues: Record<string, any>) => {
if (value !== allValues.password) {
return 'Passwords do not match.';
}
return null;
},
message: 'Passwords must match.'
},
],
},
];
// When the form is submitted with valid data:
// onSubmit({ age: '25', password: 'securepassword123', confirmPassword: 'securepassword123' })
Expected Behavior:
- If the user enters '16' for age, an error "You must be at least 18 years old." appears.
- If the user enters 'secure' for password and 'secure' for confirm password, "Password must be at least 8 characters long." appears.
- If the user enters 'securepassword' for password and 'differentpassword' for confirm password, "Passwords do not match." appears.
- The
customvalidator forconfirmPasswordshould have access to thepasswordfield's value.
Constraints
- Your
Formcomponent should be a functional component. - Use React hooks (
useState,useEffect, etc.) for state management. - The
FieldConfigtype should be a union of types for input types, and validation rules should be an array of objects, each with atypeproperty and other type-specific properties. - The
onSubmitcallback should receive an object of typeRecord<string, any>where keys are field names and values are the current form values. - Error messages should be strings.
- Consider that input values might initially be
nullorundefinedbefore user interaction. - The
customvalidator function should accept the current field's value and an object containing all current form values as arguments.
Notes
- Consider creating separate components for input fields to keep the
Formcomponent clean. - For the
type: 'email', you can reuse thepatternvalidation rule with a standard email regex. - Think about how to handle different input types (e.g.,
numberinputs might require parsing to integers or floats). - The
customvalidation rule is powerful; ensure yourFormcomponent can correctly pass all relevant form data to it. - No third-party form libraries (like Formik or React Hook Form) are allowed. You must implement the logic from scratch.