FormField.ts
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
pathwithinTOutput, 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
touchedanddirtystates 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
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
true, marks the field as touched. By default it is true, meaning the field will be marked as touched.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
true, marks the field as touched. By default it is true, meaning the field will be marked as touched.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.
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).
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
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" },
});