tuil
ReferencePackages@mwillbanks/tuil-formAPI

TerminalFormController

class exported by @mwillbanks/tuil-form.

View rawEdit

class

Public class exported by @mwillbanks/tuil-form.

export class TerminalFormController {
  readonly #fields = new Map<string, FormFieldRegistration>();
  readonly #fieldSubscriptions = new Map<string, () => void>();
  readonly #observers = new Set<() => void>();
  #submission?: AbortController;
  #version = 0;

  register(field: FormFieldRegistration): () => void {
    if (this.#fields.has(field.name)) {
      throw new Error(`Duplicate form field "${field.name}"`);
    }
    this.#fields.set(field.name, field);
    const unsubscribe = field.subscribe?.(() => this.#notify());
    if (unsubscribe) this.#fieldSubscriptions.set(field.name, unsubscribe);
    this.#notify();
    return () => {
      this.#fields.delete(field.name);
      this.#fieldSubscriptions.get(field.name)?.();
      this.#fieldSubscriptions.delete(field.name);
      this.#notify();
    };
  }

  subscribe(observer: () => void): () => void {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }

  snapshot(): number {
    return this.#version;
  }

  get dirty(): boolean {
    return [...this.#fields.values()].some((field) => field.dirty());
  }

  values(
    options: { readonly redactSecrets?: boolean } = {},
  ): Readonly<Record<string, unknown>> {
    return Object.freeze(
      Object.fromEntries(
        [...this.#fields.values()].map((field) => [
          field.name,
          field.value(options.redactSecrets ?? true),
        ]),
      ),
    );
  }

  validationSummary(): readonly FormValidationError[] {
    return Object.freeze(
      [...this.#fields.values()].flatMap((field) =>
        (field.errors?.() ?? []).map((message) =>
          Object.freeze({ field: field.name, message }),
        ),
      ),
    );
  }

  async restore(values: Readonly<Record<string, unknown>>): Promise<void> {
    for (const [name, value] of Object.entries(values)) {
      await this.#fields.get(name)?.restore?.(value);
    }
  }

  async validate(
    trigger: ValidationTrigger = "submit",
    options: {
      readonly signal?: AbortSignal;
      readonly focus?: (name: string) => void;
    } = {},
  ): Promise<boolean> {
    let firstInvalid: string | undefined;
    for (const field of this.#fields.values()) {
      options.signal?.throwIfAborted();
      const state = await field.validate(trigger, options.signal);
      if (!state.valid && firstInvalid === undefined) firstInvalid = field.name;
    }
    this.#notify();
    if (firstInvalid) options.focus?.(firstInvalid);
    return firstInvalid === undefined;
  }

  async submit<T>(
    handler: (
      values: Readonly<Record<string, unknown>>,
      signal: AbortSignal,
    ) => T | Promise<T>,
    options:
      | AbortSignal
      | {
          readonly signal?: AbortSignal;
          readonly focus?: (name: string) => void;
        } = {},
  ): Promise<T | undefined> {
    this.#submission?.abort();
    const submission = new AbortController();
    this.#submission = submission;
    const signal = options instanceof AbortSignal ? options : options.signal;
    const focus = options instanceof AbortSignal ? undefined : options.focus;
    const submissionSignal = combinedSignal(submission.signal, signal);
    if (
      !(await this.validate("submit", {
        signal: submissionSignal,
        focus,
      }))
    ) {
      return undefined;
    }
    submissionSignal.throwIfAborted();
    return handler(this.values({ redactSecrets: false }), submissionSignal);
  }

  reset(): void {
    this.#submission?.abort();
    for (const field of this.#fields.values()) field.reset();
  }

  dispose(): void {
    this.#submission?.abort();
    for (const unsubscribe of this.#fieldSubscriptions.values()) unsubscribe();
    this.#fieldSubscriptions.clear();
    this.#fields.clear();
    this.#notify();
    this.#observers.clear();
  }

  #notify(): void {
    this.#version += 1;
    for (const observer of this.#observers) observer();
  }
}

Members

MemberTypeRequiredDescriptionRelated types
#fieldsMap<string, FormFieldRegistration>YesThe #fields member uses the Map<string, FormFieldRegistration> contract.FormFieldRegistration
#fieldSubscriptionsMap<string, () => void>YesThe #fieldSubscriptions member uses the Map<string, () => void> contract.
#observersSet<() => void>YesThe #observers member uses the Set<() => void> contract.
#submissionAbortController | undefinedNoThe #submission member uses the AbortController | undefined contract.
#versionnumberYesThe #version member uses the number contract.
register(field: FormFieldRegistration) => () => voidYesThe register member uses the (field: FormFieldRegistration) => () => void contract.FormFieldRegistration
subscribe(observer: () => void) => () => voidYesThe subscribe member uses the (observer: () => void) => () => void contract.
snapshot() => numberYesThe snapshot member uses the () => number contract.
dirtybooleanYesThe dirty member uses the boolean contract.
values(options?: &#123; readonly redactSecrets?: boolean; &#125;) => Readonly<Record<string, unknown>>YesThe values member uses the (options?: &#123; readonly redactSecrets?: boolean; &#125;) => Readonly<Record<string, unknown>> contract.
validationSummary() => readonly FormValidationError[]YesThe validationSummary member uses the () => readonly FormValidationError[] contract.FormValidationError
restore(values: Readonly<Record<string, unknown>>) => Promise<void>YesThe restore member uses the (values: Readonly<Record<string, unknown>>) => Promise<void> contract.
validate(trigger?: ValidationTrigger, options?: &#123; readonly signal?: AbortSignal; readonly focus?: (name: string) => void; &#125;) => Promise<boolean>YesThe validate member uses the (trigger?: ValidationTrigger, options?: &#123; readonly signal?: AbortSignal; readonly focus?: (name: string) => void; &#125;) => Promise<boolean> contract.ValidationTrigger
submit<T>(handler: (values: Readonly<Record<string, unknown>>, signal: AbortSignal) => T | Promise<T>, options?: AbortSignal | &#123; readonly signal?: AbortSignal; readonly focus?: (name: string) => void; &#125;) => Promise<T | undefined>YesThe submit member uses the <T>(handler: (values: Readonly<Record<string, unknown>>, signal: AbortSignal) => T | Promise<T>, options?: AbortSignal | &#123; readonly signal?: AbortSignal; readonly focus?: (name: string) => void; &#125;) => Promise<T | undefined> contract.
reset() => voidYesThe reset member uses the () => void contract.
dispose() => voidYesThe dispose member uses the () => void contract.
#notify() => voidYesThe #notify member uses the () => void contract.

Parameters

This declaration has no public members.

Returns

This declaration does not return a value.

Throws

No thrown errors are documented for this declaration.

Source

View the secondary source reference

Package

@mwillbanks/tuil-form

On this page