Skip to content

Types & Events

type FormEventType =
| 'field:valid' // FieldEventDetail
| 'field:invalid' // FieldEventDetail
| 'field:change' // FieldEventDetail
| 'field:added' // FieldEventDetail
| 'field:removed' // FieldEventDetail
| 'form:valid' // FormEventDetail
| 'form:invalid' // FormEventDetail
| 'form:submit' // FormEventDetail
| 'form:loading' // FormEventDetail
| 'form:reset'; // FormEventDetail
type RegistryEventType = 'form:registered' | 'form:unregistered';

Field-Level Events (FieldControllerEventType)

Section titled “Field-Level Events (FieldControllerEventType)”
type FieldControllerEventType = 'change' | 'valid' | 'invalid';
interface FieldEventDetail {
formId: string;
fieldName: string;
state: FieldState;
}
interface FormEventDetail {
formId: string;
state: FormState;
}
interface FormLoadingStateDetail {
formId: string;
isSubmitting: boolean;
submitter: HTMLElement | null;
formEl: HTMLFormElement;
state: FormState;
}
interface FormLoadingStateOptions {
attribute?: string;
submitSelector?: string;
}
interface FieldErrorRenderContext {
message: string;
index: number;
errors: string[];
}
interface FieldControllerOptions {
validate?(
value: string,
rules: ValidatorRule[],
defaultValidate: () => FieldValidationResult
): FieldValidationResult;
onServerErrors?(errors: string[], fieldName: string): string[];
errorsSelector?: string;
findErrorsElement?(field: FieldController): HTMLElement | null;
renderError?(ctx: FieldErrorRenderContext, field: FieldController): string;
errorsSeparator?: string;
renderErrors?(errors: string[], ctx: FieldController): void;
}
interface FormControllerOptions {
fieldSelector?: string;
fieldOptions?: FieldControllerOptions;
fieldsMap?: CustomFieldsMap;
loadingState?: false | FormLoadingStateOptions;
onLoadingStateChange?: (detail: FormLoadingStateDetail) => void;
onFormInvalid?: (detail: FormEventDetail) => void;
}

The contract implemented by FieldController and custom field types. Any class conforming to this interface can participate in a FormController.

interface FormField {
readonly name: string;
getState(): FieldState;
validate(): FieldValidationResult;
reset(): void;
destroy(): void;
setEnabled(enabled: boolean): void;
setServerErrors(errors: string[]): void;
connect(onChange: (state: FieldState) => void): void;
focus(): void;
}
type FormFieldClass = new (wrapper: HTMLElement, options?: Record<string, unknown>) => FormField;
type FormFieldFactory =
| FormFieldClass
| (() => Promise<{ default: FormFieldClass }>);
type CustomFieldsMap = Record<string, FormFieldFactory>;
type FieldEventHandler = (detail: FieldEventDetail) => void;
type FormLevelEventHandler = (detail: FormEventDetail) => void;
type FormEventHandler = FieldEventHandler | FormLevelEventHandler;
type RegistryEventHandler = (detail: { formId: string }) => void;
type FieldControllerEventHandler = (state: FieldState) => void;
interface FieldState {
name: string;
value: string;
isValid: boolean;
isDirty: boolean;
isTouched: boolean;
errors: string[];
}
interface FieldValidationResult {
isValid: boolean;
errors: string[];
}
interface FormState {
id: string;
isValid: boolean;
isSubmitting: boolean;
isDirty: boolean;
fields: Record<string, FieldState>;
}
interface FormPlugin {
init(formEl: HTMLFormElement, api: FormPluginHost): void | Promise<void>;
destroy(): void;
}
type FormPluginFactory = () => Promise<{ default: new () => FormPlugin }>;

Given to form plugins during init():

interface FormPluginHost {
readonly id: string;
getFieldValue(name: string): string | undefined;
getFieldNames(): string[];
setFieldEnabled(name: string, enabled: boolean): void;
on(event: 'field:*', handler: FieldEventHandler): void;
on(event: 'form:*', handler: FormLevelEventHandler): void;
off(event: 'field:*', handler: FieldEventHandler): void;
off(event: 'form:*', handler: FormLevelEventHandler): void;
}

Custom fields implement the FormField interface. Register globally with registerFieldType() or per-form via fieldsMap:

interface FormField {
readonly name: string;
getState(): FieldState;
validate(): FieldValidationResult;
reset(): void;
destroy(): void;
setEnabled(enabled: boolean): void;
setServerErrors(errors: string[]): void;
connect(onChange: (state: FieldState) => void): void;
focus(): void;
}
type FormFieldClass = new (wrapper: HTMLElement, options?: FieldOptions) => FormField;
type FormFieldFactory = FormFieldClass | (() => Promise<{ default: FormFieldClass }>);
type CustomFieldsMap = Record<string, FormFieldFactory>;
interface FormSubmitActions {
fallbackToNative(): void;
applyValidationErrors(errors: Record<string, string[]>): void;
redirect(url: string): void;
finish(html?: string): void;
}
interface FormSubmitContext extends FormSubmitActions {
formEl: HTMLFormElement;
formData: FormData;
submitter: HTMLElement | null;
signal: AbortSignal;
}
type FormSubmitFunction = (context: FormSubmitContext) => Promise<void>;
interface Typo3FormsOptions {
disableDefaultValidators?: boolean;
additionalValidators?: Validator[];
additionalFieldTypes?: CustomFieldsMap;
additionalFormPlugins?: FormPluginFactory[];
fieldsMap?: CustomFieldsMap;
onSubmit?: FormSubmitFunction;
formSelector?: string;
fieldSelector?: string;
hooks?: Typo3FormsHooks;
}
interface Typo3FormsHooks {
onFormRegistered?(api: FormControllerApi): void;
onBeforeSubmit?(context: FormSubmitContext): boolean | void;
onAfterSubmit?(response: Typo3AjaxFormResponse, formEl: HTMLFormElement): void;
onStepChange?(page: { current: number; total: number }, formEl: HTMLFormElement): void;
onFormFinished?(response: Typo3AjaxFormResponse, formEl: HTMLFormElement): void;
onValidationError?(errors: Record<string, string[]>, formEl: HTMLFormElement): void;
onSubmitError?(error: Error, formEl: HTMLFormElement): void;
}
interface Typo3AjaxFormResponse {
valid: boolean;
errors: Record<string, string[]>;
page: { current: number; total: number };
finished: boolean;
redirect: string | null;
message: string | null;
state: string;
html?: string | null;
}
type Typo3RemountFn = (oldFormEl: HTMLFormElement, html: string) => HTMLFormElement | null;
type Typo3UnmountFn = (formEl: HTMLFormElement) => void;
interface Typo3FormsApi {
registry: FormRegistry;
destroy(): void;
}

These are not exported from the barrel but can be imported by path:

const CSS_CLASSES = {
errorClass: 'is-invalid',
errorMsgClass: 'invalid-feedback',
descriptionClass: 'form-text',
} as const;
const SELECTORS = {
formField: '[data-form-field]',
input: 'input, select, textarea',
} as const;
const DEBOUNCE_MS = 300;