Skip to content

FormControllerApi

A FormControllerApi is returned by formRegistry.register() or formRegistry.get(). It controls a single <form> element.

Pass options as the third argument to formRegistry.register(), or via the controllerOptions field of formRegistry.init():

interface FormControllerOptions {
fieldSelector?: string;
/** Options applied to every default FieldController in this form. */
fieldOptions?: FieldControllerOptions;
/** Map `data-field-type` values to custom FormField implementations. */
fieldsMap?: CustomFieldsMap;
loadingState?: false | FormLoadingStateOptions;
onLoadingStateChange?: (detail: FormLoadingStateDetail) => void;
/** Called when the form fails validation (client- or server-side). Complements form:invalid. */
onFormInvalid?: (detail: FormEventDetail) => void;
}
interface FormLoadingStateOptions {
attribute?: string; // default: 'data-loading'
submitSelector?: string; // fallback when no submitter
}

A CustomFieldsMap maps data-field-type attribute values to custom FormField implementations. Each entry can be a class (sync) or a lazy factory (async):

import type { CustomFieldsMap } from 'formlayer';
const fieldsMap: CustomFieldsMap = {
combobox: ComboboxField,
datepicker: () => import('./fields/datepicker'),
};

When a [data-form-field] wrapper has a matching data-field-type, the custom class is instantiated instead of the default FieldController. Fields without a matching entry fall back to FieldController.

See Plugins for full usage examples and Loading State for loading UI.

PropertyTypeDescription
idstringThe form’s id attribute

Returns a snapshot of a field’s state, or undefined.

const field = form.getField('email');
// { name: 'email', value: 'test@...', isValid: true, isDirty: true, isTouched: true, errors: [] }

Returns the full form state.

const state: FormState = form.getState();
// { id: 'contact', isValid: true, isSubmitting: false, isDirty: true, fields: { ... } }

Validates all fields. Returns a promise resolving to true if all fields are valid. Focuses the first invalid field.

const isValid = await form.validate();

Triggers a programmatic form submission (equivalent to the user clicking submit).

form.submit();

Resets all fields to their default DOM values and clears validation state.

form.reset();

Destroys the controller, aborts listeners, disconnects the mutation observer, and destroys all plugins.

form.destroy();

Adds a pre-constructed FormField instance to the form. If a field with the same name exists, the old one is destroyed first.

form.addField(myCustomField);

Creates and adds a field from a [data-form-field] wrapper element. Respects the form’s fieldsMap when resolving the field type.

const field = form.addFieldFromElement(wrapperEl);

Destroys and removes a field by name.

form.removeField('phone');

on(event, handler) / once(event, handler) / off(event, handler)

Section titled “on(event, handler) / once(event, handler) / off(event, handler)”

Subscribe to form and field events. Typed overloads ensure correct handler signatures.

// Field events receive FieldEventDetail
form.on('field:change', (detail) => {
detail.fieldName; // string
detail.state; // FieldState
});
// Form events receive FormEventDetail
form.on('form:submit', (detail) => {
detail.state; // FormState (isSubmitting is true)
});
form.on('form:loading', (detail) => {
detail.state.isSubmitting; // true at start, false at end
});
// once() fires only once then auto-removes
form.once('form:valid', (detail) => { ... });
interface FormState {
id: string;
isValid: boolean;
isSubmitting: boolean;
isDirty: boolean;
fields: Record<string, FieldState>;
}
interface FieldState {
name: string;
value: string;
isValid: boolean;
isDirty: boolean;
isTouched: boolean;
errors: string[];
}

The submit function receives this context object with both data and action methods:

interface FormSubmitContext {
formEl: HTMLFormElement;
formData: FormData;
submitter: HTMLElement | null;
signal: AbortSignal;
fallbackToNative(): void;
applyValidationErrors(errors: Record<string, string[]>): void;
redirect(url: string): void;
finish(html?: string): void;
}

The generic finish() action destroys the controller and optionally replaces the form element. The TYPO3 layer handles finish/unmount separately via unmount — see TYPO3 Setup.