Headless Core

FormField.ts

Reference for the FormField class

Overview

export class FormField<TOutput extends object, TValue>
  • TOutputrepresents the final validated shape of your form data. It defines the full object structure that the form is expected to produce after successful validation and submission.
  • TValuerepresents the type of the value stored in this specific field. It corresponds to the leaf value at the given path within TOutput, allowing type-safe access, updates, and validation for this field.

FormField<TOutput, TValue> is a single, type-safe form field connected to a FormController. It encapsulates the field’s value, validation state, and UI binding while keeping the controller’s data in sync.

Key responsibilities include:

  • Exposing the current, initial, and default value of the field
  • Tracking touched and dirty states independently
  • Accessing validation issues specific to the field
  • Binding/unbinding to a DOM element for focus management and scroll behavior
  • Modifying, resetting, or validating its value in a controlled manner
  • Emitting fine-grained events that propagate through the parent form

In essence, FormField provides a reactive, self-contained interface for one form value while remaining fully integrated with FormController, enabling predictable and granular form management.


Fields

📘 this.controller

controller: FormController<TOutput>;
  • TOutput*from FormField's generics (See #Overview)

Returns the parent FormController instance that this field is registered to, allowing access to shared form state, methods, and events.

📘 this.path

path: FieldPath.Segments;

Returns the path segments that identify this field within the form's data structure. This path is used for value access, updates, and validation.

📘 this.stringPath

stringPath: string;

Returns the string representation of the field's path, typically in dot notation (e.g. "user.address.street").

📘 this.value

value: TValue;
  • TOutput*from FormField's generics (See #Overview)

Returns the current value of the field, which may be modified through user input or programmatically via setValue or modifyValue.

📘 this.initialValue

initialValue: TValue;

Returns the initial value of the field as determined at registration time. This value is used for resetting the field and determining dirty state.

📘 this.boundElement

boundElement: HTMLElement | null;

Returns the currently bound DOM element for this field, if any. This element is used for focus management and scroll behavior during validation.

📘 this.issues

issues: readonly StandardSchemaV1.Issue[];

Returns an immutable list of current issues. (See Validation page)

📘 this.isTouched

isTouched: boolean;

Returns whether the field has been touched (focused and blurred) by the user.

📘 this.isDirty

isDirty: boolean;

Returns whether the field's current value differs from its initial value.

📘 this.isValid

isValid: boolean;

Returns whether the field currently has no validation issues.

Methods

📙 bindElement()

declare function bindElement(el: HTMLElement | undefined): void;

Binds a DOM element to this field for focus management and scroll behavior during validation.

Passing undefined will unbind the element.

Arguments

el!
HTMLElement | undefined
The DOM element to bind to this field.

Examples

field.bindElement(document.getElementById("my-input")!);
field.bindElement(undefined); // unbind

📙 unbindElement()

declare function unbindElement(): void;

Unbinds any currently bound DOM element from this field.

Is a convenience method equivalent to bindElement(undefined).

📙 clearIssues()

declare function clearIssues(): void;

Clears all validation issues associated with this field.

📙 setValue()

declare function setValue(
  value: TValue,
  opts?: {
    shouldTouch?: boolean;
    shouldMarkDirty?: boolean;
  },
): void;

Sets the field's value to the provided value, with optional flags to mark the field as touched or dirty.

Arguments

value!
TValue
The value to set for this field.
opts.shouldTouch?
boolean
If true, marks the field as touched. By default it is true, meaning the field will be marked as touched.
opts.shouldMarkDirty?
boolean
If true, marks the field as dirty. By default it is true, meaning the field will be marked as dirty if the new value differs from the initial value.

Examples

field.setValue("new value");
field.setValue("new value", { shouldTouch: false });
field.setValue("new value", { shouldMarkDirty: false });

📙 modifyValue()

declare function modifyData(
  draftConsumer: (draft: Draft<typeof this.controller.data>) => void,
  opts?: {
    shouldTouch?: boolean;
    shouldMarkDirty?: boolean;
  },
): void;

Allows modifying the field's value using an Immer draft of the form's data, with optional flags to mark the field as touched or dirty.

Arguments

draftConsumer!
(draft: Draft<typeof this.controller.data>) => void
The function that receives the draft of the form's data to modify.
opts.shouldTouch?
boolean
If true, marks the field as touched. By default it is true, meaning the field will be marked as touched.
opts.shouldMarkDirty?
boolean
If true, marks the field as dirty. By default it is true, meaning the field will be marked as dirty if the new value differs from the initial value.

Examples

field.modifyData((data) => {
  data.user.name = "new name";
});
field.modifyData((data) => {
  data.tags.push("new tag");
});
field.modifyData((data) => {
  data.items[0].quantity += 1;
});

📙 reset()

declare function reset(): void;

Resets the field's value back to its initial value and clears touched/dirty states.

📙 touch()

declare function touch(): void;

Marks the field as touched, indicating that the user has interacted with it.

Examples

const inputEl = document.getElementById("my-input")! as HTMLInputElement;

inputEl.onfocus = (e) => {
  field.touch(); // <-- Marks the field as touched when the input gains focus.
};

📙 markDirty()

declare function markDirty(): void;

Marks the field as dirty, indicating that its value has been modified from the initial value.

This method forces the field into a dirty state regardless of whether the current value actually differs from the initial value. Dirty state will still be calculated once setValue or modifyValue is called, so the field may become not dirty again if the value matches the initial value. Use with caution.

Examples

formController.events.on("fieldValueChanged", (fieldPath) => {
  if (fieldPath === "user.email") {
    const field = formController.getField("user.email");
    field.markDirty(); // <-- Forces "user.email" field to be dirty whenever its value changes, regardless of whether it actually differs from the initial value.
  }
});
const inputEl = document.getElementById("my-input")! as HTMLInputElement;

inputEl.onchange = (e) => {
  const value = inputEl.value;
  field.setValue(value);
  field.markDirty(); // <-- Forces dirty state regardless of whether `value` actually differs from the initial value.
};

📙 validate()

declare function validate(): Promise<void>;

Triggers validation for this field, causing the controller to re-run validation rules and update issues on this field accordingly. Equivalent to calling formController.validateField(this.path).

This method will only update issues related to this field, not the entire form. Use FormController.validate() to validate all fields and update the form's overall validity.

Returns

Promise<void>;

Resolves once validation completes and issues are updated for this field.

Examples

const inputEl = document.getElementById("my-input")! as HTMLInputElement;

// Validate onBlur
inputEl.onblur = (e) => {
  field.validate(); // <-- Validates the field when the input loses focus.
};

// Validate onChange
formController.events.on("fieldValueChanged", (fieldPath) => {
  const field = formController.getField(fieldPath);
  field.validate(); // <-- Validates the field whenever its value changes.
});
field.validate().then(() => {
  if (field.isValid) {
    console.log("Field is valid!");
  } else {
    console.log("Field has issues:", field.issues);
  }
});

📙 focus()

declare function focus(opts?: {
  shouldTouch?: boolean;
  scrollOptions?: ScrollIntoViewOptions;
}): void;

Focuses the currently bound DOM element for this field, if any. This is typically used during validation to scroll to and focus the first invalid field.

Arguments

opts.shouldTouch
boolean | undefined
Whether to also mark the field as touched when focusing it.
opts.scrollOptions
ScrollIntoViewOptions | undefined
Options to pass to Element.scrollIntoView when focusing the element.

Examples

const inputEl = document.getElementById("my-input")! as HTMLInputElement;

field.bindElement(inputEl);

field.focus({
  shouldTouch: true,
  scrollOptions: { behavior: "smooth", block: "center" },
});
Copyright © 2026