TerminalFormController
class exported by @mwillbanks/tuil-form.
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
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#fields | Map<string, FormFieldRegistration> | Yes | The #fields member uses the Map<string, FormFieldRegistration> contract. | FormFieldRegistration |
#fieldSubscriptions | Map<string, () => void> | Yes | The #fieldSubscriptions member uses the Map<string, () => void> contract. | — |
#observers | Set<() => void> | Yes | The #observers member uses the Set<() => void> contract. | — |
#submission | AbortController | undefined | No | The #submission member uses the AbortController | undefined contract. | — |
#version | number | Yes | The #version member uses the number contract. | — |
register | (field: FormFieldRegistration) => () => void | Yes | The register member uses the (field: FormFieldRegistration) => () => void contract. | FormFieldRegistration |
subscribe | (observer: () => void) => () => void | Yes | The subscribe member uses the (observer: () => void) => () => void contract. | — |
snapshot | () => number | Yes | The snapshot member uses the () => number contract. | — |
dirty | boolean | Yes | The dirty member uses the boolean contract. | — |
values | (options?: { readonly redactSecrets?: boolean; }) => Readonly<Record<string, unknown>> | Yes | The values member uses the (options?: { readonly redactSecrets?: boolean; }) => Readonly<Record<string, unknown>> contract. | — |
validationSummary | () => readonly FormValidationError[] | Yes | The validationSummary member uses the () => readonly FormValidationError[] contract. | FormValidationError |
restore | (values: Readonly<Record<string, unknown>>) => Promise<void> | Yes | The restore member uses the (values: Readonly<Record<string, unknown>>) => Promise<void> contract. | — |
validate | (trigger?: ValidationTrigger, options?: { readonly signal?: AbortSignal; readonly focus?: (name: string) => void; }) => Promise<boolean> | Yes | The validate member uses the (trigger?: ValidationTrigger, options?: { readonly signal?: AbortSignal; readonly focus?: (name: string) => void; }) => Promise<boolean> contract. | ValidationTrigger |
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> | Yes | The submit member uses the <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> contract. | — |
reset | () => void | Yes | The reset member uses the () => void contract. | — |
dispose | () => void | Yes | The dispose member uses the () => void contract. | — |
#notify | () => void | Yes | The #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.
Related types
Source
View the secondary source reference