All files / core/src form.ts

100% Statements 33/33
100% Branches 9/9
100% Functions 1/1
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 591x 1x 1x                                       1x 8x 8x 8x 8x 8x 8x   8x 2x   2x 2x 4x   4x 4x 4x 4x 4x 2x 2x   8x 15x 7x 7x 8x   8x 1x 1x 8x   8x 8x  
import { onCleanup } from './lifecycle.js';
import { exception, model } from './ref.js';
import { subscribe } from './subscription.js';
import type { ExceptionMap, ExceptionType, LinkableSchema, ModelInput, ModelOutput, StateChange } from './types.js';
 
export type FormOptions = {
  onChange?: (event: StateChange) => void;
  safeInit?: boolean;
};
 
/**
 * Creates a form with validation based on the provided schema.
 *
 * @template S - The linkable schema type
 * @template T - The model input type based on the schema
 * @param schema - The validation schema to use for the form
 * @param init - Initial values for the form
 * @param options - Optional configuration for the form
 * @returns A tuple containing:
 *   - state: Mutable form state that can be updated by user input
 *   - errors: Validation errors for the form fields
 */
export function form<S extends LinkableSchema, T extends ModelInput<S>>(
  schema: S,
  init: T,
  options?: FormOptions
): [ModelOutput<S>, ExceptionMap<ModelOutput<S>>] {
  const state = model(schema, init, { safeParse: true });
  const { errors, destroy } = exception(state);
 
  if (options?.safeInit === false) {
    const initParse = schema.safeParse(init);
 
    if (!initParse.success) {
      for (const issue of initParse.error.issues) {
        const key = issue.path.join('.');
 
        (errors as ExceptionMap<Record<string, unknown>>)[key as never] = {
          issues: [issue],
          message: issue.message,
        } as ExceptionType;
      }
    }
  }
 
  const unsubscribe = subscribe(state, (_c, event) => {
    if (event.type !== 'init') {
      options?.onChange?.(event);
    }
  });
 
  onCleanup(() => {
    unsubscribe();
    destroy();
  });
 
  return [state, errors];
}