# Form

**A powerful form component with client-side validation, AJAX submission, and seamless Extbase integration.**

**Reference:** [View Source](https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Primitives/Form)

**FormExample.html**

```html
<ui:exposeToClient />

<ui:form.root action="homepage" autoMount="{false}" 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:input.root type="url">
                <ui:input.input pattern="https?://.*" placeholder="https://example.com" />
            </ui:input.root>
            <ui:field.helperText>Use `https://down.example.com` to preview the generic error state.</ui:field.helperText>
            <ui:field.errorText />
        </ui:field.root>

        <f:variable name="submitLabelId" value="{ui:id()}" />
        <ui:button
            type="submit"
            focusableWhenDisabled="{true}"
            aria-labelledby="{submitLabelId}"
            autoMount="{false}"
            rootId="example-form-submit">
            <span id="{submitLabelId}">
                <ui:button.indicator state="{f:constant(name: 'Jramke\FluidPrimitives\Enum\ButtonIndicatorState::Idle')}">Submit</ui:button.indicator>
                <ui:button.indicator state="{f:constant(name: 'Jramke\FluidPrimitives\Enum\ButtonIndicatorState::Loading')}">Submitting...</ui:button.indicator>
            </span>
        </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" />

```

**FormExample.ts**

```ts
import { getHydrationData, mount, mountAll } from 'fluid-primitives';
import { Button } from 'fluid-primitives/button';
import { Form } from 'fluid-primitives/form';

mountAll('ui:formExample', () => {
    const data = getHydrationData('ui:form', 'example-form');
    if (!data) return;

    const submitButton = mount('ui:button', 'example-form-submit', ({ props }) => {
        const button = new Button(props);
        button.init();
        return button;
    });

    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 => submitButton?.api.setLoading(form.api.isSubmitting),
    });

    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
- Field validation that follows the validation mode, from native constraints, your schema or callback, and async checks of single fields
- 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](/docs/core-concepts/forms.md) for a complete form integration.

## Anatomy

```html
<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.errorText />
        </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>
```

## Installation

```bash
typo3 ui:add form
```

Or copy the files manually from GitHub (https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Registry/Form) into your project.

Read more about installing [Components and Primitives](/docs/core-concepts/primitives.md).

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`. Fields read their value `inputDebounceMs` after the last change (100 by default, never less than 50), so typing does not validate on every keystroke.

## API Reference

### form.root

Submits and manages the form state. Renders a `<form>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `actionUri` | `string` | No | `-` | The resolved form action URI to submit to directly. |
| `action` | `string` | No | `-` | The Extbase action name used to build the form action URI. |
| `extensionName` | `string` | No | `-` | The Extbase extension name used to build the form action URI. |
| `pluginName` | `string` | No | `-` | The Extbase plugin name used to build the form action URI. |
| `controller` | `string` | No | `-` | The Extbase controller name used to build the form action URI. |
| `arguments` | `array` | No | `-` | The Extbase arguments used to build the form action URI. |
| `pageUid` | `int` | No | `-` | The target page UID used to build the form action URI. |
| `objectName` | `string` | No | `-` | The object name prefix used for nested form field names. |
| `object` | `mixed` | No | `-` | The bound object used for form value mapping. |
| `method` | `string` | No | `'post'` | The HTTP method used for form submission. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `rootId` | `string` | No | `-` | The root ID of the component, used for hydration and identification. |
| `ids` | `array` | No | `[]` | The IDs of of the component parts for composition. |
| `autoMount` | `boolean` | No | `true` | Whether the client initializes the component on its own. Set it to false to mount it yourself with `mount()` |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-state` | The state of the form. One of: "invalid", "ready", "submitting", "success", "error". |
| `data-submitting` | Present while the form is being submitted. |
| `data-invalid` | Present when invalid |
| `data-dirty` | Present when dirty |
| `data-touched` | Present when touched |

### 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.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### form.indicator

Displays content for an exact form state such as `error`, `success`, or `submitting`. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `state` | `Enum\FormState` | Yes | `-` | The form state to render. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### form.errorText

Displays the current form-level error text set through the Form API, or its slotted fallback text. Renders a `<span>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### form.successText

Displays the current form-level success text set through the Form API, or its slotted fallback text. Renders a `<span>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### Machine options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | `-` | The unique identifier of the form. |
| `validation` | `FormValidation \| undefined` | `-` | Validates the values of the form on the client: a Standard Schema validator such as Zod, or a synchronous callback that gets the `values` and returns the errors by field name. It cannot be set from a Fluid template, only by constructing `Form` in your own entry file. |
| `objectName` | `string \| undefined` | `-` | The object name that prefixes the names of nested form fields. |
| `inputDebounceMs` | `number \| undefined` | `100` | How long the fields wait after the last change before they read their value, never less than 50. A `validate` that sends a request should not fire on every keystroke. |
| `onSubmit` | `(({ values, api, event, post }: { values: FormValues; api: FormApi; event: JSX.FormEvent<HTMLElement>; post: (url: string) => Promise<Response>; }) => Promise<FormSubmitResult> \| FormSubmitResult) \| undefined` | `-` | Called with the values once the form is valid and submitted. Return `true` for success, `false` for an error, or field errors to mark the form invalid. `post(url)` submits the form as `FormData` and maps a 422 JSON response to field errors for you. |
| `render` | `((form: Form) => void) \| undefined` | `-` | Called every time the form state changes. Use it to update UI the form does not wire itself. |

### Machine JavaScript API

| Name | Type | Description |
| --- | --- | --- |
| `isSubmitting` | `boolean` | Whether the form is being submitted. |
| `isDirty` | `boolean` | Whether the value of any field differs from the one it started with. |
| `isInvalid` | `boolean` | Whether any field is invalid. |
| `isSuccessful` | `boolean` | Whether the last submit succeeded. |
| `isError` | `boolean` | Whether the last submit ended in an error. |
| `getValues` | `() => FormValues` | The current values of the form. |
| `getErrors` | `() => FormErrors` | The errors of the invalid fields, by field name. |
| `getDirty` | `() => FormDirty` | The dirty fields, by field name. |
| `getTouched` | `() => FormTouched` | The touched fields, by field name. |
| `getErrorText` | `() => string \| null` | The text of the error status of the form, `null` if there is none. |
| `setErrorText` | `(text: string \| null) => void` | Sets the text of the error status. Pass `null` to remove it. |
| `getSuccessText` | `() => string \| null` | The text of the success status of the form, `null` if there is none. |
| `setSuccessText` | `(text: string \| null) => void` | Sets the text of the success status. Pass `null` to remove it. |
| `clearStatusText` | `() => void` | Removes the error and the success text. |
| `getAllFields` | `() => Map<string, FieldHandle>` | Every field of the form, by name. |
| `getField` | `(name: string) => FieldHandle \| undefined` | The field with this name, `undefined` if the form has none. |
| `getFormEl` | `() => HTMLFormElement \| null` | The `<form>` element. |
| `getAction` | `() => string` | The `action` of the form, an empty string if it has none. |
| `reset` | `() => void` | Resets the form to its initial state. |
| `syncFields` | `() => void` | Picks up fields that were added or removed after the form was hydrated. |
| `renameField` | `(oldName: string, newName: string) => void` | Renames a field, keeping its value, touched, dirty and error state. Useful to keep the rows of a FieldArray contiguously indexed after one in the middle was removed. |
