Field Array
View as MarkdownA repeatable group of fields, for letting users add and remove rows like "add another person".
Features
Permalink to heading "Features"- Add and remove rows client-side, with contiguous
name[0],name[1], ... indexing maintained automatically as rows are removed - Existing rows render for real on the server - no JavaScript required to see or submit data that's already there
- Works with
Fieldand any Field-aware primitive (Input, Select, Checkbox, ...) nested inside a row - Row
names use the same bracket notation (people[0][firstName]) Extbase andFormValuesalready understand - Optional
minItems/maxItemsbounds -addTrigger/removeTriggerdisable themselves once a bound is reached, andappend()/remove()are no-ops past it emptyState/addTrigger/removeTriggeralready reflect the correct hidden/disabled state in the server-rendered HTML, fromitemCount, not only once JavaScript hydrates
<primitives:fieldArray.root itemCount="{items -> f:count()}">
<primitives:fieldArray.itemTemplate>
<primitives:fieldArray.item>
<!-- Your row's fields here -->
<primitives:fieldArray.removeTrigger />
</primitives:fieldArray.item>
</primitives:fieldArray.itemTemplate>
<primitives:fieldArray.itemGroup>
<f:for each="{items}" as="item">
<primitives:fieldArray.item index="{...}">
<!-- Your row's fields here -->
<primitives:fieldArray.removeTrigger />
</primitives:fieldArray.item>
</f:for>
<primitives:fieldArray.emptyState />
</primitives:fieldArray.itemGroup>
<primitives:fieldArray.addTrigger />
</primitives:fieldArray.root>
Installation
Permalink to heading "Installation"typo3 ui:add field-array
Please copy the files manually from GitHub into your project.
Read more about installing Components and Primitives.
FieldArray only manages rows and indexing - each row's own fields still need the Field primitive (typo3 ui:add field) nested inside.
How It Works
Permalink to heading "How It Works"A row's markup is authored once, inside itemTemplate - a <template> element that's never rendered directly, only cloned client-side whenever addTrigger is clicked. Existing rows are a separate, ordinary loop over data you already have, rendered for real:
<primitives:fieldArray.itemTemplate>
<primitives:fieldArray.item>
<!-- one row's fields, authored once -->
</primitives:fieldArray.item>
</primitives:fieldArray.itemTemplate>
<primitives:fieldArray.itemGroup>
<f:for each="{people}" as="person" iteration="it">
<primitives:fieldArray.item index="{it.index}">
<!-- the same row markup, authored again with this person's data -->
</primitives:fieldArray.item>
</f:for>
</primitives:fieldArray.itemGroup>
Yes, the row is written twice - once for the stencil, once for the loop. This isn't a shortcut taken for FieldArray specifically; it's the same pattern FileUpload already uses for mixing already-uploaded files with newly-picked ones (see its docs' "Editing" section), and it's what keeps every row genuinely server-rendered rather than reconstructed from JSON after the page loads.
Each fieldArray.item's index prop (omitted inside itemTemplate, where no real index exists yet) automatically prefixes every nested Field's name - <ui:field name="firstName"> inside row 1 of an array named people becomes people[1][firstName] without you writing that out yourself.
Rows and the Trusted Properties of an Extbase Form
Permalink to heading "Rows and the Trusted Properties of an Extbase Form"Inside a Form bound to a persisted object, the form signs the field names of every row rendered on the server into __trustedProperties (people[0][firstName], people[1][firstName], ...). The stencil in itemTemplate is left out, because its index is only known once the client adds the row. Extbase rejects a name that is not signed for a persisted object, so allow the properties of the rows added on the client in the property mapping configuration of the argument, e.g. forProperty('people.*')->allowAllProperties() in the initialize...Action.
Reflecting Row Count on the Server
Permalink to heading "Reflecting Row Count on the Server"root takes a required itemCount prop - the number of rows you're about to render in the loop above. Nothing counts your rows for you (they're your own f:for loop, not a collection FieldArray owns), so pass it explicitly, e.g. itemCount="{people -> f:count()}". It's what lets emptyState/addTrigger/removeTrigger start in the correct hidden/disabled state in the server-rendered HTML itself, matching minItems/maxItems, rather than only correcting themselves once JavaScript hydrates.
<primitives:fieldArray.root name="people" itemCount="{people -> f:count()}">
<!-- ... -->
</primitives:fieldArray.root>
Examples
Permalink to heading "Examples"Starting Empty
Permalink to heading "Starting Empty"A FieldArray with no rows yet - emptyState shows until the first row is added.
Limiting Row Count
Permalink to heading "Limiting Row Count"minItems="1" disables removeTrigger once a single row remains; maxItems="3" disables addTrigger once three rows exist. The status text below the rows ("2 of 3 added") isn't a FieldArray feature by itself - it's a plain element the row markup authors itself ({ui:ref(name: 'status', context: 'fieldArray')}), kept in sync from onItemAdded/onItemRemoved the same way this example already mounts each row's own Field/Input.
Rows With Portalled Components
Permalink to heading "Rows With Portalled Components"A row that composes components rendering their content through ui:portal: a Select, a DatePicker and a Popover. A portal inside itemTemplate renders in place, wrapped in <div data-portal>, because the stencil is inert. When a row is added, the clone moves the wrapper to where the portal wants to go - the end of <body>, or after the ui:portalContainer of its name - and removing the row removes it again. A ui:portal with disabled stays in the row. The row's entry file mounts the components the clone brought along with mountAll. The row is written once, in an <f:section name="row"> that the stencil and the loop both f:render - a long row does not have to be maintained twice.
Full Form With Client-Side Validation
Permalink to heading "Full Form With Client-Side Validation"A complete Form wrapping a FieldArray of guests, each with a name/email pair - required fields and the email format are validated live as you type or blur, using a Zod schema passed straight to validation. z.array(z.object({...})) covers however many guest rows currently exist (added or removed) without FieldArray needing to know about validation at all - each issue's own path (e.g. ['guests', 0, 'email']) is matched back to the exact row's own field automatically. Submission is blocked until every row is valid.
API Reference
Permalink to heading "API Reference"The following tables cover the available props of the Fluid Primitives.
fieldArray.root
Permalink to heading "fieldArray.root"Contains every part of the field array. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
name | stringThe base field name for this array's rows, e.g. `people` - each row's nested fields are automatically prefixed with it and their row index, producing names like `people[0][firstName]`. | Yes | - |
itemCount | integerNumber of rows rendered server-side, e.g. `itemCount=""`. Lets `emptyState`/`addTrigger`/`removeTrigger` reflect the correct hidden/disabled state before JavaScript hydrates, instead of only afterward. | Yes | - |
minItems | integerMinimum number of rows required - once exactly this many remain, removing a row is disabled. | No | - |
maxItems | integerMaximum number of rows allowed - once this many exist, adding a row is disabled. | No | - |
translations | arrayLocalized screen-reader announcements for adding/removing a row. A `%number%` placeholder is replaced with the row's 1-based position. Set an entry to `` to disable that announcement, or (client-side only, via the FieldArray constructor) pass a callback to build the message from the row's own field values instead. Use `f:translate` for per-template localization overrides when needed. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
rootId | stringThe root ID of the component, used for hydration and identification. | No | - |
ids | arrayThe IDs of of the component parts for composition. | No | [] |
autoMount | booleanWhether the client initializes the component on its own. Set it to false to mount it yourself with `mount()` | No | true |
class | stringThe CSS class(es) to be applied to the component. | No | - |
fieldArray.itemTemplate
Permalink to heading "fieldArray.itemTemplate"Wraps one row's markup, authored once and cloned client-side for each added row. Renders a <template> element - never visible itself.
fieldArray.itemGroup
Permalink to heading "fieldArray.itemGroup"Groups every row, existing and added. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
fieldArray.item
Permalink to heading "fieldArray.item"One row. Existing rows are authored directly with an index; added rows are cloned from itemTemplate, which omits it. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
index | integerThis row's position in the array, e.g. `0`. Required for a real row; omitted inside `itemTemplate`'s stencil, where the client fills in a real index on each clone. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
fieldArray.emptyState
Permalink to heading "fieldArray.emptyState"Shown while itemGroup has no rows yet. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
fieldArray.addTrigger
Permalink to heading "fieldArray.addTrigger"Appends a new row, cloned from itemTemplate. Renders a <button> element.
| Name | Description | Required | Default |
|---|---|---|---|
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
Rendered data attributes
| Attribute | Description |
|---|---|
data-disabled | Present when disabled |
fieldArray.removeTrigger
Permalink to heading "fieldArray.removeTrigger"Removes its enclosing row and re-indexes every later row down by one. Renders a <button> element.
| Name | Description | Required | Default |
|---|---|---|---|
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
Rendered data attributes
| Attribute | Description |
|---|---|
data-disabled | Present when disabled |
data-value | The index of the row the trigger removes. |
Machine options
Permalink to heading "Machine options"| Name | Description | Default |
|---|---|---|
id | stringThe unique identifier of the field array. | - |
name | stringThe name the rows are nested under, e.g. people for people[0][firstName]. | - |
itemCount | number | undefinedThe number of rows the server rendered. It sets the state of the triggers before hydration, once the client runs it counts the rows in the DOM instead. | - |
minItems | number | undefinedThe fewest rows allowed. At this many, remove() and the remove triggers do nothing and the remove triggers are disabled. | |
maxItems | number | undefinedThe most rows allowed. At this many, append() and the add trigger do nothing and the add trigger is disabled. | - |
translations | { rowAdded?: FieldArrayAnnounceTranslation; rowRemoved?: FieldArrayAnnounceTranslation; } | undefinedThe screen reader announcements of an added and a removed row. In a string, %number% is replaced with the 1-based position of the row, e.g. Row %number% removed.. A callback builds the message from the data of the row, and may use %number% in what it returns. | - |
onItemAdded | ((detail: { index: number; componentNames: string[] }) => void) | undefinedCalled after a row was added by the add trigger, with its index and the client names of the components inside it, e.g. ['field', 'input']. Call their mountAll() here: the field array doesn't know what the template of a row contains. | - |
onItemRemoved | ((detail: { index: number }) => void) | undefinedCalled after a row was removed by its remove trigger, with the index it had. The later rows have already moved up by then. | - |
Machine JavaScript API
Permalink to heading "Machine JavaScript API"| Name | Type | Description |
|---|---|---|
getRows | () => { index: number }[] | The rows that currently exist, by index. |
canAppend | () => boolean | Whether append() would currently add a row, i.e. maxItems hasn't been reached. |
canRemove | () => boolean | Whether remove() would currently remove a row, i.e. more than minItems remain. |
append | () => void | Adds a row, if maxItems allows it. |
remove | (index: number) => void | Removes the row at index, if minItems allows it. |