Hone logo
Hone
Problems

Advanced Form State Management in Vue with TypeScript

This challenge focuses on building a robust and scalable form state management system for a Vue.js application using TypeScript. Effective form state management is crucial for handling user input, validation, and submission efficiently, especially in complex applications with multiple fields and dynamic behavior.

Problem Description

Your task is to create a reusable form state management solution for a Vue.js application that utilizes TypeScript. This solution should abstract away the complexities of managing form data, validation errors, and submission status, allowing developers to easily integrate and use forms in their applications.

Key requirements:

  • State Management: The solution must provide a centralized way to manage the form's data (values for each field).
  • Validation: Implement a mechanism for defining and running field-level and form-level validations. Errors should be clearly tracked and accessible.
  • Submission Handling: Provide a way to trigger form submission and track its status (e.g., isSubmitting, submissionError).
  • Reusability: The solution should be designed as a composable function (using Vue 3's Composition API) to promote reusability across different forms.
  • TypeScript Integration: All aspects of the solution must be strongly typed using TypeScript for improved developer experience and code safety.
  • Dynamic Forms: The solution should be flexible enough to handle forms with a variable number of fields.

Expected behavior:

  • Users should be able to input data into form fields.
  • Fields should display validation errors when rules are violated.
  • The form should prevent submission if there are validation errors.
  • A submission process can be initiated, and its state (loading, success, error) should be manageable.

Important edge cases to consider:

  • Forms with no fields.
  • Fields with no validation rules.
  • Asynchronous validation rules.
  • Resetting form state.

Examples

Example 1: Basic Text Input and Validation

Consider a simple registration form with email and password fields.

// Assume this is within a Vue component using the composable
const { formData, errors, validateField, isFormValid, handleSubmit } = useForm({
  initialData: {
    email: '',
    password: '',
  },
  validationRules: {
    email: [
      { rule: (value: string) => value.includes('@'), message: 'Email must contain an @ symbol' },
      { rule: (value: string) => value.length > 5, message: 'Email must be at least 5 characters long' },
    ],
    password: [
      { rule: (value: string) => value.length >= 8, message: 'Password must be at least 8 characters long' },
    ],
  },
});

// In the template:
// v-model="formData.email"
// @blur="validateField('email')"
// :error-message="errors.email" (if you have a component to display errors)

// On submit button click:
// @click="handleSubmit(async () => { console.log('Form submitted:', formData); })"

Input:

  • Initial form data: { email: '', password: '' }
  • Validation rules for email:
    • Must contain '@'
    • Length > 5
  • Validation rules for password:
    • Length >= 8

Scenario:

  1. User types test into the email field.
  2. User types pass into the password field.
  3. User attempts to submit.

Expected State After Input and Before Submit:

{
  "formData": {
    "email": "test",
    "password": "pass"
  },
  "errors": {
    "email": "Email must contain an @ symbol", // Fails first rule
    "password": "Password must be at least 8 characters long" // Fails rule
  },
  "isFormValid": false
}

Expected Behavior on Submit Attempt:

Submission handler is not called.

Example 2: Form with Dynamic Fields and Submission

Imagine a product form where fields can be added dynamically.

// Assume this is within a Vue component using the composable
const { formData, errors, addField, removeField, validateForm, handleSubmit, isSubmitting } = useForm({
  initialData: {
    productName: '',
  },
  validationRules: {
    productName: [{ rule: (value: string) => value.length > 0, message: 'Product name is required' }],
  },
});

// Dynamically add a 'price' field with validation
function addPriceField() {
  addField('price', {
    initialValue: 0,
    validationRules: [
      { rule: (value: number) => value > 0, message: 'Price must be greater than 0' },
    ],
  });
}

// Handle submission
const onSubmit = async () => {
  const isValid = await validateForm(); // Validate all fields
  if (isValid) {
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('Product saved:', formData.value);
      alert('Product saved successfully!');
    } catch (e) {
      console.error('Submission failed:', e);
      // Handle submission error appropriately
    }
  }
};

// In the template:
// <button @click="addPriceField">Add Price</button>
// v-model="formData.productName"
// v-for="field in dynamicFields" :key="field.name"
//   <input v-model="formData[field.name]" @blur="validateField(field.name)" :error-message="errors[field.name]" />
// <button @click="handleSubmit(onSubmit)" :disabled="isSubmitting">
//   {{ isSubmitting ? 'Saving...' : 'Save Product' }}
// </button>

Input:

  • Initial form data: { productName: '' }
  • Validation rules for productName: required.

Scenario:

  1. User adds the price field.
  2. User enters Awesome Widget for productName.
  3. User enters 50 for price.
  4. User clicks the submit button.

Expected State During Submission:

  • isSubmitting becomes true.
  • validateForm() is called.
  • If valid, the onSubmit function runs, simulating an API call.
  • After successful simulation, isSubmitting becomes false.

Expected Output on Successful Submission:

  • Console logs: Product saved: { productName: 'Awesome Widget', price: 50 }
  • Alert: Product saved successfully!

Example 3: Asynchronous Validation and Reset

Consider a signup form with an email confirmation field that requires an asynchronous check against a backend service.

// Assume this is within a Vue component using the composable
const { formData, errors, validateField, resetForm, handleSubmit } = useForm({
  initialData: {
    email: '',
    confirmEmail: '',
  },
  validationRules: {
    email: [
      { rule: (value: string) => value.includes('@'), message: 'Email must contain an @ symbol' },
    ],
    confirmEmail: [
      { rule: (value: string) => value === formData.value.email, message: 'Emails do not match' },
      {
        rule: async (value: string) => {
          // Simulate API call to check email availability
          await new Promise(resolve => setTimeout(resolve, 500));
          return value !== 'admin@example.com';
        },
        message: 'This email is already taken.',
        isAsync: true, // Mark as asynchronous
      },
    ],
  },
});

// On blur of confirmEmail:
// validateField('confirmEmail'); // This should trigger async validation if it's the only error type

// On submit attempt:
// handleSubmit(async () => { ... });

// To reset:
// resetForm();

Input:

  • Initial form data: { email: '', confirmEmail: '' }
  • Validation rules for email: must contain '@'.
  • Validation rules for confirmEmail:
    • Must match email.
    • Asynchronously check if admin@example.com.

Scenario:

  1. User types user@example.com into email.
  2. User types user@example.com into confirmEmail.
  3. User triggers validation on confirmEmail.
  4. User then types admin@example.com into confirmEmail.
  5. User triggers validation on confirmEmail again.

Expected State After Step 3 (after async validation finishes):

{
  "formData": {
    "email": "user@example.com",
    "confirmEmail": "user@example.com"
  },
  "errors": {}, // Assuming email validation passed
  "isFormValid": true // Assuming email validation passed
}

Expected State After Step 5 (after async validation finishes):

{
  "formData": {
    "email": "user@example.com",
    "confirmEmail": "admin@example.com"
  },
  "errors": {
    "confirmEmail": "This email is already taken."
  },
  "isFormValid": false
}

Resetting the form:

Calling resetForm() should revert formData to its initialData and clear all errors.

Constraints

  • The solution must be implemented using Vue 3's Composition API.
  • The solution must be written entirely in TypeScript.
  • The validation rules should support synchronous and asynchronous validation functions.
  • The state management should be efficient and avoid unnecessary re-renders.
  • The solution should be testable in a Vue testing environment.

Notes

  • Consider how to handle different input types (text, numbers, booleans, arrays, objects).
  • Think about how to provide a clear API for defining validation rules, including custom messages.
  • The handleSubmit function should accept a callback that is executed only if the form is valid.
  • Consider how to expose the current formData to the component for v-model binding.
  • You may want to consider a way to manage per-field validation states (e.g., isFieldValid, isFieldTouched).
  • The problem statement is intentionally broad to allow for creative solutions. Focus on the core requirements of state management, validation, and submission.
  • A good solution will be well-documented and easy to understand for other developers.
Loading editor...
typescript