Form
A powerful form component with client-side validation, AJAX submission, and seamless Extbase integration.
<ui:exposeToClient />
<ui:form.root action="homepage" controlled="{true}" rootId="example-form" class="min-w-[275px]">
<ui:form.content class="space-y-4">
<ui:field.root name="homepage" required="{true}">
<ui:field.label>Homepage</ui:field.label>
<ui:field.control asChild="{true}">
<ui:input type="url" pattern="https?://.*" placeholder="https://example.com" />
</ui:field.control>
<ui:field.description>Use `https://down.example.com` to preview the generic error state.</ui:field.description>
<ui:field.error />
</ui:field.root>
<ui:button type="submit" attributes="{ui:ref(name: 'submit-button', asArray: '{true}')}">Submit</ui:button>
</ui:form.content>
<ui:form.indicator
state="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FormState::Error')}"
class="space-y-3">
<p class="font-medium">Submission failed</p>
<ui:form.errorText class="block">The demo server is unavailable right now.</ui:form.errorText>
<ui:button type="reset">Try again</ui:button>
</ui:form.indicator>
<ui:form.indicator
state="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FormState::Success')}"
class="space-y-3">
<p class="font-medium">Submitted</p>
<ui:form.successText class="block">Your homepage was submitted successfully.</ui:form.successText>
<ui:button type="reset">Submit another</ui:button>
</ui:form.indicator>
</ui:form.root>
<vite:asset entry="EXT:docs/Resources/Private/Components/FormExample/FormExample.entry.ts" />
import { getHydrationData, mount } from 'fluid-primitives';
import { Form } from 'fluid-primitives/form';
mount('form-example', () => {
const data = getHydrationData('form', 'example-form');
if (!data) return;
const form = new Form({
...data.props,
validation: ({ values }) => {
const homepage = values.get('homepage');
if (typeof homepage !== 'string' || homepage.trim() === '') {
return {
homepage: { messages: ['Please enter an url.'] },
};
}
try {
new URL(homepage);
} catch {
return {
homepage: { messages: ['Please enter a valid url.'] },
};
}
if (homepage === 'https://example.com') {
return {
homepage: { messages: ['The example homepage is not allowed.'] },
};
}
},
onSubmit: async ({ api, post }) => {
const [response] = await Promise.all([
post(api.getAction()),
new Promise(resolve => setTimeout(resolve, 800)),
]);
const data = await response.json();
if (!response.ok) {
api.setErrorText(data.message || 'The demo server is unavailable right now.');
return false;
}
api.setSuccessText(data.message || 'Your homepage was submitted successfully.');
return true;
},
render: form => {
form.api
.getFormEl()
?.querySelector('button[type="submit"]')
?.setAttribute('aria-disabled', form.api.isSubmitting ? 'true' : 'false');
},
});
form.init();
});
Features
- Optional client-side validation with Standard Schema-compatible validators or custom callbacks
- AJAX form submission with automatic error handling
- Seamless Extbase controller integration
- Real-time field validation on blur and input
- Form state management (submitting, dirty, invalid, success, error)
- Built-in primitives for editable content, state indicators, and status text
- Works with all Field-aware components and basic HTML inputs
- Automatic field name prefixing for Extbase
See the complete Form Guide for a complete form integration.
Installation
typo3 ui:add form
Please copy the files manually from GitHub into your project.
Read more about installing Components and Primitives.
Client-side validation is configured in your entry file with the validation option, not in the Fluid template. Pass either a Standard Schema-compatible validator such as Zod or a synchronous callback that reads the values object. values uses your field names as dot paths such as person.name, keeps leaf values as string | File, exposes arrays through getAll(), and can be converted to a nested object with toObject() or partially read with pick(). Validation and server errors stay flat and field-keyed, so nested fields still return errors under keys such as person.name. For submit results, return true, false, or field errors from onSubmit, and use api.setErrorText() or api.setSuccessText() for form-level messages. Use post(url) to submit the current form as FormData.
API Reference
The following tables cover the available props of the Fluid Primitives.
form.root
Submits and manages the form state. Renders a <form> element.
| Name | Description | Required | Default |
|---|---|---|---|
actionUri | stringThe resolved form action URI to submit to directly. | No | - |
action | stringThe Extbase action name used to build the form action URI. | No | - |
extensionName | stringThe Extbase extension name used to build the form action URI. | No | - |
pluginName | stringThe Extbase plugin name used to build the form action URI. | No | - |
controller | stringThe Extbase controller name used to build the form action URI. | No | - |
arguments | arrayThe Extbase arguments used to build the form action URI. | No | - |
pageUid | intThe target page UID used to build the form action URI. | No | - |
objectName | stringThe object name prefix used for nested form field names. | No | - |
object | mixedThe bound object used for form value mapping. | No | - |
method | stringThe HTTP method used for form submission. | No | 'post' |
form.content
Wraps the editable form UI. It stays visible in ready, invalid, and submitting, and hides in error and success. Renders a <div> element.
form.indicator
Displays content for an exact form state such as error, success, or submitting. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
state | Enum\FormStateThe form state to render. | Yes | - |
form.errorText
Displays the current form-level error text set through the Form API, or its slotted fallback text. Renders a <span> element.
form.successText
Displays the current form-level success text set through the Form API, or its slotted fallback text. Renders a <span> element.
Anatomy
<primitives:form.root>
<primitives:form.content>
<primitives:field.root>
<primitives:field.label />
<primitives:field.control asChild="{true}">
<!-- Your form input here -->
</primitives:field.control>
<primitives:field.error />
</primitives:field.root>
</primitives:form.content>
<primitives:form.indicator state="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FormState::Error')}">
<primitives:form.errorText />
</primitives:form.indicator>
</primitives:form.root>