TerminalFieldController
class exported by @mwillbanks/tuil-form.
class
Public class exported by @mwillbanks/tuil-form.
export class TerminalFieldController<T> {
#options: TerminalFieldOptions<T>;
#state: TerminalFieldState<T>;
readonly #observers = new Set<FieldObserver>();
#validation?: AbortController;
#validationSequence = 0;
constructor(options: TerminalFieldOptions<T>) {
this.#options = options;
const value = options.value ?? options.initialValue;
this.#state = Object.freeze({
name: options.name,
value,
initialValue: options.initialValue,
errors: [],
touched: false,
dirty: !Object.is(value, options.initialValue),
validating: false,
disabled: options.disabled ?? false,
readOnly: options.readOnly ?? false,
valid: true,
});
}
get state(): TerminalFieldState<T> {
return this.#state;
}
get secret(): boolean {
return this.#options.secret ?? false;
}
subscribe(observer: FieldObserver): () => void {
this.#observers.add(observer);
return () => this.#observers.delete(observer);
}
configure(options: TerminalFieldOptions<T>): void {
this.#options = options;
const externalValue = options.value;
const nextValue =
externalValue === undefined ? this.#state.value : externalValue;
const next = {
...this.#state,
name: options.name,
value: nextValue,
initialValue: options.initialValue,
dirty: !Object.is(nextValue, options.initialValue),
disabled: options.disabled ?? false,
readOnly: options.readOnly ?? false,
};
if (
next.name !== this.#state.name ||
!Object.is(next.value, this.#state.value) ||
!Object.is(next.initialValue, this.#state.initialValue) ||
next.dirty !== this.#state.dirty ||
next.disabled !== this.#state.disabled ||
next.readOnly !== this.#state.readOnly
) {
this.#setState(next);
}
}
async setValue(
value: T,
options: {
readonly validate?: boolean;
readonly signal?: AbortSignal;
} = {},
): Promise<TerminalFieldState<T>> {
if (this.#state.disabled || this.#state.readOnly) return this.#state;
this.#setState({
...this.#state,
value,
dirty: !Object.is(value, this.#state.initialValue),
});
await this.#options.onValueChange?.(value);
return options.validate === false
? this.#state
: this.validate("change", options.signal);
}
async blur(signal?: AbortSignal): Promise<TerminalFieldState<T>> {
this.#setState({ ...this.#state, touched: true });
return this.validate("blur", signal);
}
async validate(
trigger: ValidationTrigger = "command",
signal?: AbortSignal,
): Promise<TerminalFieldState<T>> {
const validators = validatorsFor(this.#options.validators, trigger);
this.#validation?.abort();
const validation = new AbortController();
this.#validation = validation;
const validationSignal = combinedSignal(validation.signal, signal);
const sequence = ++this.#validationSequence;
if (trigger === "submit") {
this.#setState({ ...this.#state, touched: true, validating: true });
} else {
this.#setState({ ...this.#state, validating: true });
}
const errors: string[] = [];
try {
for (const validator of validators) {
validationSignal.throwIfAborted();
const error = await validator(this.#state.value, {
signal: validationSignal,
trigger,
});
validationSignal.throwIfAborted();
if (error) errors.push(error);
}
} catch (error) {
if (sequence === this.#validationSequence) {
this.#setState({ ...this.#state, validating: false });
}
if (validationSignal.aborted) return this.#state;
throw error;
}
if (sequence !== this.#validationSequence) return this.#state;
this.#setState({
...this.#state,
errors: Object.freeze(errors),
validating: false,
valid: errors.length === 0,
});
return this.#state;
}
setErrors(errors: readonly string[]): void {
this.#setState({
...this.#state,
errors: Object.freeze([...errors]),
valid: errors.length === 0,
validating: false,
});
}
reset(value: T = this.#state.initialValue): void {
this.#validation?.abort();
this.#validationSequence += 1;
this.#setState({
...this.#state,
value,
initialValue: value,
errors: [],
touched: false,
dirty: false,
validating: false,
valid: true,
});
}
serialize(redactSecrets = true): T | "[REDACTED]" {
return redactSecrets && this.secret ? "[REDACTED]" : this.#state.value;
}
dispose(): void {
this.#validation?.abort();
this.#observers.clear();
}
#setState(state: TerminalFieldState<T>): void {
this.#state = Object.freeze(state);
for (const observer of this.#observers) observer();
}
}Members
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#options | TerminalFieldOptions<T> | Yes | The #options member uses the TerminalFieldOptions<T> contract. | TerminalFieldOptions |
#state | TerminalFieldState<T> | Yes | The #state member uses the TerminalFieldState<T> contract. | TerminalFieldState |
#observers | Set<FieldObserver> | Yes | The #observers member uses the Set<FieldObserver> contract. | — |
#validation | AbortController | undefined | No | The #validation member uses the AbortController | undefined contract. | — |
#validationSequence | number | Yes | The #validationSequence member uses the number contract. | — |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
state | TerminalFieldState<T> | Yes | The state member uses the TerminalFieldState<T> contract. | TerminalFieldState |
secret | boolean | Yes | The secret member uses the boolean contract. | — |
subscribe | (observer: FieldObserver) => () => void | Yes | The subscribe member uses the (observer: FieldObserver) => () => void contract. | — |
configure | (options: TerminalFieldOptions<T>) => void | Yes | The configure member uses the (options: TerminalFieldOptions<T>) => void contract. | TerminalFieldOptions |
setValue | (value: T, options?: { readonly validate?: boolean; readonly signal?: AbortSignal; }) => Promise<TerminalFieldState<T>> | Yes | The setValue member uses the (value: T, options?: { readonly validate?: boolean; readonly signal?: AbortSignal; }) => Promise<TerminalFieldState<T>> contract. | TerminalFieldState |
blur | (signal?: AbortSignal) => Promise<TerminalFieldState<T>> | Yes | The blur member uses the (signal?: AbortSignal) => Promise<TerminalFieldState<T>> contract. | TerminalFieldState |
validate | (trigger?: ValidationTrigger, signal?: AbortSignal) => Promise<TerminalFieldState<T>> | Yes | The validate member uses the (trigger?: ValidationTrigger, signal?: AbortSignal) => Promise<TerminalFieldState<T>> contract. | TerminalFieldState, ValidationTrigger |
setErrors | (errors: readonly string[]) => void | Yes | The setErrors member uses the (errors: readonly string[]) => void contract. | — |
reset | (value?: T) => void | Yes | The reset member uses the (value?: T) => void contract. | — |
serialize | (redactSecrets?: boolean) => T | "[REDACTED]" | Yes | The serialize member uses the (redactSecrets?: boolean) => T | "[REDACTED]" contract. | — |
dispose | () => void | Yes | The dispose member uses the () => void contract. | — |
#setState | (state: TerminalFieldState<T>) => void | Yes | The #setState member uses the (state: TerminalFieldState<T>) => void contract. | TerminalFieldState |
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