Skip to main content

Field Array

Preview
View as Markdown

A repeatable group of fields, for letting users add and remove rows like "add another person".

The fields in a row are Fields, so add both:

Read more about installing Components and Primitives.

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.

With itemCount="0", the emptyState shows until the first row is added.

No people added yet.

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.

1 of 3 added (2 remaining)

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.

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.

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.

The following tables cover the available props of the Fluid Primitives.

Contains every part of the field array. Renders a <div> element.

NameDescriptionRequiredDefault
namestring
The 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
-
itemCountinteger
Number 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
-
minItemsinteger
Minimum number of rows required - once exactly this many remain, removing a row is disabled.
No
-
maxItemsinteger
Maximum number of rows allowed - once this many exist, adding a row is disabled.
No
-
translationsarray
Localized 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
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
rootIdstring
The root ID of the component, used for hydration and identification.
No
-
idsarray
The IDs of of the component parts for composition.
No
[]
autoMountboolean
Whether the client initializes the component on its own. Set it to false to mount it yourself with `mount()`
No
true
providerstring
The `id` of the `.provider` this root takes its `rootId` and props from.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Wraps one row's markup, authored once and cloned client-side for each added row. Renders a <template> element - never visible itself.

Groups every row, existing and added. Renders a <div> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

One row. Existing rows are authored directly with an index; added rows are cloned from itemTemplate, which omits it. Renders a <div> element.

NameDescriptionRequiredDefault
indexinteger
This 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
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Shown while itemGroup has no rows yet. Renders a <div> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Appends a new row, cloned from itemTemplate. Renders a <button> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when disabled

Removes its enclosing row and re-indexes every later row down by one. Renders a <button> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when disabled
data-valueThe index of the row the trigger removes.
NameDescriptionDefault
idstring
The unique identifier of the field array.
-
namestring
The name the rows are nested under, e.g. people for people[0][firstName].
-
itemCountnumber | undefined
The 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.
-
minItemsnumber | undefined
The fewest rows allowed. At this many, remove() and the remove triggers do nothing and the remove triggers are disabled.
maxItemsnumber | undefined
The most rows allowed. At this many, append() and the add trigger do nothing and the add trigger is disabled.
-
translations{ rowAdded?: FieldArrayAnnounceTranslation; rowRemoved?: FieldArrayAnnounceTranslation; } | undefined
The 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) | undefined
Called 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) | undefined
Called after a row was removed by its remove trigger, with the index it had. The later rows have already moved up by then.
-
NameTypeDescription
getRows() => { index: number }[]The rows that currently exist, by index.
canAppend() => booleanWhether append() would currently add a row, i.e. maxItems hasn't been reached.
canRemove() => booleanWhether remove() would currently remove a row, i.e. more than minItems remain.
append() => voidAdds a row, if maxItems allows it.
remove(index: number) => voidRemoves the row at index, if minItems allows it.