tuil
ReferencePackages@mwillbanks/tuil-formAPI

TerminalFieldController

class exported by @mwillbanks/tuil-form.

View rawEdit

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

MemberTypeRequiredDescriptionRelated types
#optionsTerminalFieldOptions<T>YesThe #options member uses the TerminalFieldOptions<T> contract.TerminalFieldOptions
#stateTerminalFieldState<T>YesThe #state member uses the TerminalFieldState<T> contract.TerminalFieldState
#observersSet<FieldObserver>YesThe #observers member uses the Set<FieldObserver> contract.
#validationAbortController | undefinedNoThe #validation member uses the AbortController | undefined contract.
#validationSequencenumberYesThe #validationSequence member uses the number contract.
__constructoranyYesThe __constructor member uses the any contract.
stateTerminalFieldState<T>YesThe state member uses the TerminalFieldState<T> contract.TerminalFieldState
secretbooleanYesThe secret member uses the boolean contract.
subscribe(observer: FieldObserver) => () => voidYesThe subscribe member uses the (observer: FieldObserver) => () => void contract.
configure(options: TerminalFieldOptions<T>) => voidYesThe configure member uses the (options: TerminalFieldOptions<T>) => void contract.TerminalFieldOptions
setValue(value: T, options?: &#123; readonly validate?: boolean; readonly signal?: AbortSignal; &#125;) => Promise<TerminalFieldState<T>>YesThe setValue member uses the (value: T, options?: &#123; readonly validate?: boolean; readonly signal?: AbortSignal; &#125;) => Promise<TerminalFieldState<T>> contract.TerminalFieldState
blur(signal?: AbortSignal) => Promise<TerminalFieldState<T>>YesThe blur member uses the (signal?: AbortSignal) => Promise<TerminalFieldState<T>> contract.TerminalFieldState
validate(trigger?: ValidationTrigger, signal?: AbortSignal) => Promise<TerminalFieldState<T>>YesThe validate member uses the (trigger?: ValidationTrigger, signal?: AbortSignal) => Promise<TerminalFieldState<T>> contract.TerminalFieldState, ValidationTrigger
setErrors(errors: readonly string[]) => voidYesThe setErrors member uses the (errors: readonly string[]) => void contract.
reset(value?: T) => voidYesThe reset member uses the (value?: T) => void contract.
serialize(redactSecrets?: boolean) => T | "[REDACTED]"YesThe serialize member uses the (redactSecrets?: boolean) => T | "[REDACTED]" contract.
dispose() => voidYesThe dispose member uses the () => void contract.
#setState(state: TerminalFieldState<T>) => voidYesThe #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.

Source

View the secondary source reference

Package

@mwillbanks/tuil-form

On this page