Docs/enterprisev1.0.0-rc.1

Form Management

Overview

FormManager<T> provides reactive, type-safe form state management. It tracks field values, per-field validation errors, touched status, submission state, and overall validity without coupling to any rendering layer. A subscribe() method lets UI components or stores react to state changes.

Key Concepts

  • FieldConfig describes one field: its name, initial value, whether it is required, and an array of validation rules.
  • FormState<T> is the complete reactive snapshot: values, errors, touched flags, isSubmitting, and isValid.
  • Validation runs on every setValue() call for the changed field and on all fields when submit() is called. Submission is blocked when any field is invalid.
  • validationRules is a built-in collection of common rules (email, minLength, maxLength, pattern) you can compose without writing custom validators.

API Reference

FieldConfig<T>

interface FieldConfig<T = any> {
  name: string;
  initialValue: T;
  validationRules?: ValidationRule<T>[];
  required?: boolean;
}

ValidationRule<T>

interface ValidationRule<T = any> {
  validate: (value: T) => boolean;
  message: string;
}

FormState<T>

interface FormState<T extends Record<string, any>> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  isSubmitting: boolean;
  isValid: boolean;
}

FormManager<T>

class FormManager<T extends Record<string, any>> {
  constructor(fields: FieldConfig[])

  getState(): FormState<T>
  setValue<K extends keyof T>(name: K, value: T[K]): void
  setTouched<K extends keyof T>(name: K, touched?: boolean): void
  submit(onSubmit: (values: T) => Promise<void> | void): Promise<void>
  reset(): void
  subscribe(listener: (state: FormState<T>) => void): () => void   // returns unsubscribe
}

function createForm<T extends Record<string, any>>(fields: FieldConfig[]): FormManager<T>

Built-in validationRules

const validationRules = {
  email: ValidationRule<string>
  minLength: (min: number) => ValidationRule<string>
  maxLength: (max: number) => ValidationRule<string>
  pattern: (regex: RegExp, message: string) => ValidationRule<string>
}

Basic Usage

import { createForm, validationRules } from '@tagna/udiot';

interface LoginValues {
  email: string;
  password: string;
}

const form = createForm<LoginValues>([
  {
    name: 'email',
    initialValue: '',
    required: true,
    validationRules: [validationRules.email],
  },
  {
    name: 'password',
    initialValue: '',
    required: true,
    validationRules: [validationRules.minLength(8)],
  },
]);

// React to state changes (e.g., re-render a UI component)
const unsubscribe = form.subscribe((state) => {
  console.log('isValid:', state.isValid, 'errors:', state.errors);
});

// Update a field value (triggers validation for that field)
form.setValue('email', 'user@example.com');
form.setTouched('email');

// Submit — validates all fields, calls handler only if all are valid
await form.submit(async (values) => {
  await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify(values),
    headers: { 'Content-Type': 'application/json' },
  });
});

unsubscribe();

Common Patterns

Custom validation rules

import { createForm, FieldConfig, ValidationRule } from '@tagna/udiot';

const strongPassword: ValidationRule<string> = {
  validate: (v) => /[A-Z]/.test(v) && /[0-9]/.test(v) && v.length >= 12,
  message: 'Password must be 12+ chars and contain uppercase and a digit',
};

const form = createForm<{ password: string }>([
  { name: 'password', initialValue: '', required: true, validationRules: [strongPassword] },
]);

Connecting form state to a store

import { createForm, ReactiveStore, Store, Mutation } from '@tagna/udiot';

interface ProfileState { name: string; email: string; dirty: boolean; }

@ReactiveStore({ name: 'ProfileStore' })
class ProfileStore extends Store<ProfileState> {
  constructor() { super({ name: '', email: '', dirty: false }); }

  @Mutation()
  sync(values: Partial<ProfileState>): void {
    this.dispatch({ type: 'SYNC', payload: values });
  }
}

const profileStore = new ProfileStore({ name: '', email: '', dirty: false });

const form = createForm<{ name: string; email: string }>([
  { name: 'name', initialValue: '', required: true },
  { name: 'email', initialValue: '', required: true },
]);

form.subscribe((state) => {
  if (state.isValid) {
    profileStore.sync({ ...state.values, dirty: true });
  }
});

Reset on successful submission

await form.submit(async (values) => {
  await apiService.createUser(values);
  form.reset(); // clear values, errors, touched flags
});

Integration Notes

  • FormManager is framework-agnostic — it does not import from the DI or state modules. Wrap it in an @Injectable() service when you need it injected.
  • isSubmitting is set to true at the start of submit() and back to false in a finally block, even if the onSubmit handler throws. Always check isSubmitting to disable submit buttons.
  • All fields are marked as touched when submit() is called, so error messages appear for untouched required fields on first submit attempt.
  • The subscribe callback is called synchronously on every setValue, setTouched, submit, and reset call.

Pitfalls

  • validationRules.email is a rule object, not a factory. Use it directly: validationRules: [validationRules.email].
  • validationRules.minLength, maxLength, and pattern are factories that return a new rule each time. Call them: validationRules.minLength(8).
  • Field names in FieldConfig.name must match the keys of T exactly. TypeScript will catch mismatches when you use setValue<K extends keyof T>.
  • FormManager does not debounce validation. For expensive async validators, wrap setValue calls externally with a debounce utility.
From the canonical UDIOT public documentation.Try it in the playground ↗