Skip to content

TYPO3 Setup

The TYPO3 integration layer wraps the generic FormLayer library with TYPO3-specific defaults. A single initTypo3Forms() call handles everything.

import { initTypo3Forms } from 'formlayer/typo3';
import { registerComboboxPlugin } from 'formlayer-plugin-combobox';
import { registerClientVariantsPlugin } from 'formlayer-plugin-client-variants';
import { registerDatepickerPlugin } from 'formlayer-plugin-datepicker';
import { registerTypo3AltchaPlugin } from 'formlayer-plugin-altcha/typo3';
registerComboboxPlugin();
registerClientVariantsPlugin();
registerDatepickerPlugin();
registerTypo3AltchaPlugin();
const api = initTypo3Forms();

Enable Use AJAX submission on the form content element in the TYPO3 backend (FlexForm checkbox). This activates the custom Fluid templates and hidden fields required for AJAX.

See TYPO3 Backend (PHP) for middleware, templates, and server-side configuration.

  1. Registers all 12 built-in validators
  2. Creates an AJAX submit handler (createTypo3Submit) that sends X-Form-Ajax: 1
  3. Provides remount and unmount for multistep form lifecycle
  4. Scans for form[id] elements and initializes each one
  5. Returns a Typo3FormsApi object for cleanup

Plugins (combobox, client-variants, datepicker, altcha) are separate npm packages — register them before calling initTypo3Forms(). See Plugins.

const api = initTypo3Forms({
disableDefaultValidators: false,
additionalValidators: [
{
type: 'PhoneNumber',
validate(value, options) {
if (!value) return { valid: true, message: '' };
const valid = /^\+?\d[\d\s\-()]{6,}$/.test(value);
return { valid, message: valid ? '' : (options['message'] as string) ?? 'Invalid phone number' };
},
},
],
additionalFieldTypes: {
'color-picker': () => import('./fields/color-picker'),
},
additionalFormPlugins: [
() => import('./plugins/form-analytics'),
],
// Override the submit function entirely
// onSubmit: myCustomSubmitFn,
formSelector: 'form[id]',
fieldSelector: '[data-form-field]',
hooks: {
onFormRegistered(api) {
console.log(`Form "${api.id}" ready`);
},
onBeforeSubmit(ctx) {
return false; // cancel submit
},
onAfterSubmit(response, formEl) { /* ... */ },
onValidationError(errors, formEl) { /* ... */ },
onStepChange(page, formEl) { /* new form element after remount */ },
onFormFinished(response, formEl) { /* ... */ },
onSubmitError(error, formEl) { /* ... */ },
},
});
HookWhenCan cancel?
onFormRegisteredForm controller created (also after remount)No
onBeforeSubmitBefore AJAX request is sentYes (return false)
onAfterSubmitAfter response JSON is parsedNo
onValidationErrorServer returned validation errorsNo
onStepChangeMultistep form advanced; receives remounted form elementNo
onFormFinishedServer indicated form is completeNo
onSubmitErrorNetwork failure, non-OK status, or invalid JSONNo
User clicks Submit
→ Client validation (FormController)
→ data-loading on submit button (default)
→ onBeforeSubmit hook (return false to cancel)
→ POST with X-Form-Ajax: 1 header
→ AjaxFormSubmitMiddleware (PHP) returns JSON
→ onAfterSubmit hook
→ If !valid: update __state + apply field errors + onValidationError
→ If finished: onFormFinished → unmount → redirect or replace with message
→ If multistep: remount (outerHTML) + onStepChange

TYPO3 multistep forms use registry-level lifecycle management instead of generic FormController methods:

ActionWhenWhat happens
remountValid step with html in responseUnregister old form → replace outerHTML → register new form
unmountForm finished (no redirect)Unregister form → replace with finisher message HTML

This ensures plugins, event listeners, and field controllers are properly destroyed before DOM replacement.

Generic finish() from FormSubmitContext is not used by the TYPO3 submit handler. Cleanup goes through unmount instead.

interface Typo3AjaxFormResponse {
valid: boolean;
errors: Record<string, string[]>;
page: { current: number; total: number };
finished: boolean;
redirect: string | null;
message: string | null; // finisher HTML
state: string; // HMAC-protected FormState
html?: string | null; // full <form> HTML for next step
}

Submit buttons get data-loading automatically via the generic FormController. Style with CSS:

.t3-form button[data-loading] {
opacity: 0.6;
pointer-events: none;
}

See Loading State for customization.

const api = initTypo3Forms();
api.destroy(); // unregisters all forms, removes listeners

After destroy(), you can call initTypo3Forms() again.

const api = initTypo3Forms();
const form = api.registry.get('my-form-id');
form?.on('form:loading', (detail) => {
console.log('Submitting:', detail.state.isSubmitting);
});
// Manual registration (uses same submitFn as init)
const lateForm = document.getElementById('dynamic-form') as HTMLFormElement;
api.registry.register(lateForm, createTypo3Submit());