or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

field-registration.mdform-actions.mdform-creation.mdindex.mdstate-management.mdutilities.mdvalidation.md
tile.json

validation.mddocs/

Validation

Comprehensive validation system supporting both synchronous and asynchronous validation at form and field levels with performance optimization and cross-field validation capabilities.

Capabilities

Form-Level Validation

Form-wide validation that validates all form values together.

interface Config<FormValues, InitialFormValues> {
  /** Form-level validation function */
  validate?: (values: FormValues) => ValidationErrors | Promise<ValidationErrors>;
  
  /** Whether to validate fields on blur event (default: false) */
  validateOnBlur?: boolean;
}

type ValidationErrors = AnyObject | undefined;
type AnyObject = { [key: string]: any };

Usage Examples:

import { createForm, FORM_ERROR } from "final-form";

// Synchronous form validation
const form = createForm({
  onSubmit: (values) => console.log(values),
  validate: (values) => {
    const errors = {};
    
    // Required field validation
    if (!values.email) {
      errors.email = 'Email is required';
    }
    
    // Email format validation
    if (values.email && !/\S+@\S+\.\S+/.test(values.email)) {
      errors.email = 'Invalid email format';
    }
    
    // Cross-field validation
    if (values.password !== values.confirmPassword) {
      errors.confirmPassword = 'Passwords must match';
    }
    
    // Form-level error
    if (values.username === 'admin' && values.password === 'admin') {
      errors[FORM_ERROR] = 'Invalid credentials combination';
    }
    
    return errors;
  }
});

// Asynchronous form validation
const asyncForm = createForm({
  onSubmit: async (values) => await submitUser(values),
  validate: async (values) => {
    const errors = {};
    
    // Async username availability check
    if (values.username) {
      try {
        const isAvailable = await checkUsernameAvailability(values.username);
        if (!isAvailable) {
          errors.username = 'Username is already taken';
        }
      } catch (error) {
        errors.username = 'Unable to verify username availability';
      }
    }
    
    // Async email validation
    if (values.email) {
      const isValidDomain = await validateEmailDomain(values.email);
      if (!isValidDomain) {
        errors.email = 'Email domain is not allowed';
      }
    }
    
    return errors;
  }
});

// Validation on blur
const blurValidationForm = createForm({
  onSubmit: (values) => console.log(values),
  validateOnBlur: true, // Validate fields when they lose focus
  validate: (values) => {
    const errors = {};
    if (values.name && values.name.length < 2) {
      errors.name = 'Name must be at least 2 characters';
    }
    return errors;
  }
});

Field-Level Validation

Individual field validation with support for cross-field dependencies.

interface FieldConfig<FieldValue = any> {
  /** Function that returns a validator for this field */
  getValidator?: GetFieldValidator<FieldValue>;
  
  /** Names of other fields to validate when this field changes */
  validateFields?: string[];
  
  /** Whether field validation is asynchronous */
  async?: boolean;
}

type GetFieldValidator<FieldValue = any> = () => FieldValidator<FieldValue> | undefined;

type FieldValidator<FieldValue = any> = (
  value: FieldValue,
  allValues: object,
  meta?: FieldState<FieldValue>
) => any | Promise<any>;

Usage Examples:

// Basic field validation
form.registerField(
  'email',
  (fieldState) => updateEmailField(fieldState),
  { value: true, error: true },
  {
    getValidator: () => (value, allValues) => {
      if (!value) return 'Email is required';
      if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
      
      // Access other form values for cross-field validation
      if (allValues.confirmEmail && value !== allValues.confirmEmail) {
        return 'Email addresses must match';
      }
    }
  }
);

// Async field validation
form.registerField(
  'username',
  (fieldState) => updateUsernameField(fieldState),
  { value: true, error: true, validating: true },
  {
    async: true,
    getValidator: () => async (value) => {
      if (!value) return 'Username is required';
      if (value.length < 3) return 'Username must be at least 3 characters';
      
      // Async validation
      try {
        const isAvailable = await checkUsernameAvailability(value);
        if (!isAvailable) {
          return 'Username is already taken';
        }
      } catch (error) {
        return 'Unable to validate username';
      }
    }
  }
);

// Cross-field validation with validateFields
form.registerField(
  'password',
  (fieldState) => updatePasswordField(fieldState),
  { value: true, error: true },
  {
    validateFields: ['confirmPassword'], // Re-validate confirmPassword when password changes
    getValidator: () => (value) => {
      if (!value) return 'Password is required';
      if (value.length < 8) return 'Password must be at least 8 characters';
      if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
        return 'Password must contain uppercase, lowercase, and number';
      }
    }
  }
);

form.registerField(
  'confirmPassword',
  (fieldState) => updateConfirmPasswordField(fieldState),
  { value: true, error: true },
  {
    getValidator: () => (value, allValues) => {
      if (!value) return 'Please confirm your password';
      if (value !== allValues.password) {
        return 'Passwords must match';
      }
    }
  }
);

Validation Control

Methods for controlling validation behavior programmatically.

interface FormApi<FormValues, InitialFormValues> {
  /** Check if validation is currently paused */
  isValidationPaused(): boolean;
  
  /** Pause all validation */
  pauseValidation(): void;
  
  /** Resume validation */
  resumeValidation(): void;
}

Usage Examples:

// Pause validation during bulk updates
form.pauseValidation();
form.batch(() => {
  form.change('field1', 'value1');
  form.change('field2', 'value2');
  form.change('field3', 'value3');
});
form.resumeValidation(); // Validation will run once when resumed

// Conditional validation pausing
if (isImportingData) {
  form.pauseValidation();
  await importFormData();
  form.resumeValidation();
}

// Check validation state
if (form.isValidationPaused()) {
  console.log('Validation is currently paused');
}

Error Handling Patterns

Common patterns for handling validation errors.

// Special error constants
const FORM_ERROR: string;
const ARRAY_ERROR: string;

Error Handling Examples:

import { createForm, FORM_ERROR, ARRAY_ERROR } from "final-form";

// Form-level errors
const form = createForm({
  onSubmit: async (values) => {
    try {
      await submitData(values);
    } catch (error) {
      // Return form-level error
      return { [FORM_ERROR]: 'Submission failed. Please try again.' };
    }
  },
  validate: (values) => {
    const errors = {};
    
    // Field-specific errors
    if (!values.name) {
      errors.name = 'Name is required';
    }
    
    // Form-level error for business logic
    if (values.age < 18 && values.requiresParentalConsent !== true) {
      errors[FORM_ERROR] = 'Parental consent required for users under 18';
    }
    
    return errors;
  }
});

// Array field errors
const arrayForm = createForm({
  onSubmit: (values) => console.log(values),
  validate: (values) => {
    const errors = {};
    
    if (values.items && Array.isArray(values.items)) {
      const itemErrors = values.items.map((item, index) => {
        const itemError = {};
        if (!item.name) {
          itemError.name = 'Item name is required';
        }
        if (!item.price || item.price <= 0) {
          itemError.price = 'Price must be greater than 0';
        }
        return Object.keys(itemError).length > 0 ? itemError : undefined;
      });
      
      // Check if array has any errors
      if (itemErrors.some(error => error)) {
        errors.items = itemErrors;
      }
      
      // Array-level error
      if (values.items.length === 0) {
        errors.items = { [ARRAY_ERROR]: 'At least one item is required' };
      }
    }
    
    return errors;
  }
});

// Error display patterns
form.subscribe(
  (formState) => {
    // Display form-level errors
    if (formState.error) {
      showFormError(formState.error);
    }
    
    // Display field-level errors
    if (formState.errors) {
      Object.keys(formState.errors).forEach(fieldName => {
        if (fieldName !== FORM_ERROR) {
          showFieldError(fieldName, formState.errors[fieldName]);
        }
      });
    }
  },
  { error: true, errors: true }
);

Validation Performance

Best practices for optimizing validation performance.

// ✅ Good: Memoized validators
const createEmailValidator = () => {
  const emailRegex = /\S+@\S+\.\S+/;
  return (value) => {
    if (!value) return 'Email is required';
    if (!emailRegex.test(value)) return 'Invalid email format';
  };
};

form.registerField(
  'email',
  (fieldState) => updateEmailField(fieldState),
  { value: true, error: true },
  { getValidator: createEmailValidator }
);

// ✅ Good: Debounced async validation
const createAsyncValidator = () => {
  let timeoutId;
  return async (value) => {
    if (!value) return 'Username is required';
    
    // Debounce async validation
    return new Promise((resolve) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(async () => {
        try {
          const isAvailable = await checkUsernameAvailability(value);
          resolve(isAvailable ? undefined : 'Username taken');
        } catch (error) {
          resolve('Validation error');
        }
      }, 300);
    });
  };
};

// ❌ Bad: Creating new validators on every render
form.registerField(
  'email',
  (fieldState) => updateEmailField(fieldState),
  { value: true, error: true },
  {
    getValidator: () => (value) => { // New function every time
      if (!value) return 'Email is required';
      return /\S+@\S+\.\S+/.test(value) ? undefined : 'Invalid email';
    }
  }
);

// ✅ Good: Conditional validation
const createConditionalValidator = (condition) => {
  return (value, allValues) => {
    if (!condition(allValues)) {
      return undefined; // Skip validation if condition not met
    }
    
    if (!value) return 'This field is required';
    // ... other validation logic
  };
};

form.registerField(
  'conditionalField',
  (fieldState) => updateField(fieldState),
  { value: true, error: true },
  {
    getValidator: () => createConditionalValidator(
      (values) => values.enableConditionalField === true
    )
  }
);