Hone logo
Hone
Problems

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:

  1. Generic Form Component: Create a Form component that accepts an array of field configurations. Each field configuration should define the input type, name, label, initial value, and validation rules.
  2. Input Field Types: Support at least the following input types:
    • text
    • email
    • password
    • number
  3. 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 or null.
  4. Error Display: Validation errors should be displayed clearly below the corresponding input field.
  5. 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 the Form component should be called with the current form values. If any field is invalid, errors should be displayed, and the onSubmit callback should not be triggered.
  6. State Management: Manage the state of form values and validation errors within the Form component.
  7. Reusability: The Form component 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 onSubmit function 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 onSubmit will not be called.

Edge Cases:

  • Handling empty or null initial values.
  • The pattern validation rule with various regex expressions.
  • The custom validation 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 username empty, an error "Username is required." appears.
  • If the user submits with username as "Al", an error "Username must be at least 5 characters long." appears.
  • If the user submits with username as "Alice" and bio as 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 email empty, "Email is required." appears.
  • If the user submits with email as "test@", "Invalid email address format." appears.
  • If the user submits with email as "test@example.com", the onSubmit callback 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 custom validator for confirmPassword should have access to the password field's value.

Constraints

  • Your Form component should be a functional component.
  • Use React hooks (useState, useEffect, etc.) for state management.
  • The FieldConfig type should be a union of types for input types, and validation rules should be an array of objects, each with a type property and other type-specific properties.
  • The onSubmit callback should receive an object of type Record<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 null or undefined before user interaction.
  • The custom validator 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 Form component clean.
  • For the type: 'email', you can reuse the pattern validation rule with a standard email regex.
  • Think about how to handle different input types (e.g., number inputs might require parsing to integers or floats).
  • The custom validation rule is powerful; ensure your Form component 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.
Loading editor...
typescript