Field Array
PreviewA repeatable group of fields, for letting users add and remove rows like "add another person".
Features
Permalink to heading "Features"- Add and remove rows in the browser,
name[0],name[1], ... stay contiguous as rows are removed - Existing rows are rendered on the server, so they show and submit without JavaScript
- Works with
Fieldand the field-aware primitives (Input, Select, Checkbox, ...) in a row - Row names use the bracket notation Extbase understands (
people[0][firstName]) - Optional
minItemsandmaxItems,addTriggerandremoveTriggerdisable themselves at the bound emptyState,addTriggerandremoveTriggerstart with the right hidden or disabled state in the server-rendered HTML, derived fromitemCount
<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"The fields in a row are Fields, so add both:
typo3 ui:add fieldArray field
Please copy the files manually from GitHub into your project: FieldArray, Field
Read more about installing Components and Primitives.
Examples
Permalink to heading "Examples"Render the rows you already have
Permalink to heading "Render the rows you already have"Write a row twice: once in itemTemplate, which the browser clones for every added row, and once in an f:for loop over your data, which the server renders. The demo at the top of the page renders two people this way.
Pass the index of the loop to each item as index. A Field in a row gets its name prefixed with the array name and the index, so firstName in row 1 of people becomes people[1][firstName].
Pass the number of rows you render as itemCount, e.g. itemCount="{people -> f:count()}". It lets emptyState, addTrigger and removeTrigger start with the right state in the server-rendered HTML, matching minItems and maxItems.
Fields in an added row hydrate once your entry file calls mountAll for them in onItemAdded.
Start with an empty list
Permalink to heading "Start with an empty list"With itemCount="0", the emptyState shows until the first row is added.
Limit the number of rows
Permalink to heading "Limit the number of rows"minItems="1" disables removeTrigger once one row is left, maxItems="3" disables addTrigger at three rows. The "2 of 3 added" text is your own element, marked with {ui:ref(name: 'status', context: 'fieldArray')} and updated from onItemAdded and onItemRemoved.
Add a Select, a DatePicker and a Popover to a row
Permalink to heading "Add a Select, a DatePicker and a Popover to a row"Components that render their content through ui:portal work in a row. An added row moves that content to the end of <body> (or after the ui:portalContainer of its name) and removes it again with the row. A ui:portal with disabled stays in the row.
The demo writes the row once in an <f:section name="row"> that itemTemplate and the loop both f:render, and calls mountAll for the row's components in onItemAdded.
Validate a guest list
Permalink to heading "Validate a guest list"A Form with a FieldArray of guests, each with a name and an email address. Pass a Zod schema to validation: z.array(z.object({...})) covers however many rows exist, and the path of each issue (e.g. ['guests', 0, 'email']) lands on the field of that row. The form submits once every row is valid.
Save rows to a persisted object
Permalink to heading "Save rows to a persisted object"Inside a Form bound to a persisted object, __trustedProperties signs the field names of the rows rendered on the server (guests[0][name], guests[1][name], ...). Rows added in the browser are unsigned, and Extbase rejects unsigned names for a persisted object. Allow the properties of a row in the property mapping configuration of the argument, e.g. forProperty('guests.*')->allowProperties('name', 'email') in initializeUpdateAction(). Name them, because allowAllProperties() would let a visitor set every property of a row.
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 |
provider | stringThe `id` of the `.provider` this root takes its `rootId` and props from. | No | - |
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. |