Properties
All properties are set on the render array of an element with '#type' => 'element_multiple'. The element also accepts the usual Form API properties such as #title, #description and #required.
Summary
Section titled “Summary”| Property | Type | Default | Group |
|---|---|---|---|
#element |
array | a text field | Content |
#default_value |
array | [] |
Content |
#key |
string or NULL | NULL |
Content |
#header |
bool, string, array or NULL | NULL |
Header |
#header_label |
string | '' |
Header |
#cardinality |
int or FALSE |
FALSE |
Row count |
#max_items |
int | 1000 |
Row count |
#min_items |
int or NULL | NULL |
Row count |
#empty_items |
int | 1 |
Row count |
#item_label |
string | item |
Messages |
#no_items_message |
string | No items entered. Please add items below. |
Messages |
#sorting |
bool | TRUE |
Operations |
#operations |
bool | TRUE |
Operations |
#add |
bool | TRUE |
Operations |
#remove |
bool | TRUE |
Operations |
#add_more |
bool | TRUE |
Add more |
#add_more_items |
int | 1 |
Add more |
#add_more_button_label |
string | Add |
Add more |
#add_more_input |
bool | TRUE |
Add more |
#add_more_input_label |
string | more items |
Add more |
#is_empty |
callable or NULL | NULL |
Callbacks |
#row_alter |
callable or NULL | NULL |
Callbacks |
#ajax_attributes |
array | [] |
Markup |
#table_attributes |
array | [] |
Markup |
#table_wrapper_attributes |
array | [] |
Markup |
#required, #required_error |
bool, string | Form API | Form API |
#states |
array | none | Form API |
#access |
bool | TRUE |
Form API |
#input, #process, #theme_wrappers, #markup |
internal | see below | Internal |
Content
Section titled “Content”#element
Section titled “#element”- Type: array
- Default: a text field with an invisible title and the placeholder
Enter value…
The element to repeat for each row. There are two forms.
A single element is a render array with a #type. Each row holds one input and the submitted value is a flat list.
$form['emails'] = [ '#type' => 'element_multiple', '#title' => $this->t('Email addresses'), '#element' => [ '#type' => 'email', '#title' => $this->t('Email address'), '#title_display' => 'invisible', '#placeholder' => $this->t('name@example.com'), ],];A composite element is an associative array of named child elements. Each row holds all of them and the submitted value is a list of arrays keyed by the child names.
$form['contacts'] = [ '#type' => 'element_multiple', '#title' => $this->t('Contacts'), '#header' => TRUE, '#element' => [ 'first_name' => ['#type' => 'textfield', '#title' => $this->t('First name')], 'last_name' => ['#type' => 'textfield', '#title' => $this->t('Last name')], ],];See Single element and Composite element.
#default_value
Section titled “#default_value”- Type: array
- Default: none
The initial rows. It has the same shape as the submitted value. A scalar is wrapped in an array.
$form['contacts']['#default_value'] = [ ['first_name' => 'John', 'last_name' => 'Smith'], ['first_name' => 'Jane', 'last_name' => 'Doe'],];- Type: string or
NULL - Default:
NULL
The name of a child of a composite #element. Its value becomes the array key of the row in the submitted value, and the child is removed from the row array. Keys must be unique. A duplicate key sets a form error.
#key needs a composite #element. With a single element the module throws an \InvalidArgumentException.
$form['options'] = [ '#type' => 'element_multiple', '#title' => $this->t('Options'), '#key' => 'value', '#header' => TRUE, '#element' => [ 'value' => ['#type' => 'textfield', '#title' => $this->t('Value')], 'text' => ['#type' => 'textfield', '#title' => $this->t('Text')], ], '#default_value' => [ 'one' => ['text' => 'One'], 'two' => ['text' => 'Two'], ],];See Keyed.
Header
Section titled “Header”#header
Section titled “#header”- Type:
TRUE,FALSE, string, array orNULL - Default:
NULL
Controls the table header and, for a composite element, the layout of the row.
| Value | Result |
|---|---|
NULL or FALSE |
One header cell spanning all columns. It shows #header_label, or the element title hidden for screen readers. A composite row keeps all its children in one cell. |
TRUE |
One column per visible child, titled with the child #title. Child titles are hidden inside the cells. For a single element, one column titled with the #title of #element. |
| A string | One header cell spanning all columns, showing the string. A composite row has one column per child. |
| An array | One entry per visible child. An entry is a string or a header cell array such as ['data' => 'Name', 'width' => '50%']. |
The module adds the handle, weight and operations cells around your columns.
$form['contacts']['#header'] = [ ['data' => $this->t('First name'), 'width' => '50%'], ['data' => $this->t('Last name'), 'width' => '50%'],];#header_label
Section titled “#header_label”- Type: string
- Default:
''
Text of a single header cell that spans all columns. It is used when #header is empty or TRUE and there is a label. Without it, the element #title is rendered visually hidden.
$form['tags']['#header_label'] = $this->t('Tag');Row count
Section titled “Row count”#cardinality
Section titled “#cardinality”- Type: int or
FALSE - Default:
FALSE
The maximum number of rows. FALSE means no limit. The constant ElementMultiple::CARDINALITY_UNLIMITED (-1) means the same. When the limit is reached, the add buttons disappear. #min_items and #empty_items never exceed it. The server holds the limit too: a submission with more rows is cut at it, see #max_items.
use Drupal\element_multiple\Element\ElementMultiple;
$form['phones']['#cardinality'] = 3;$form['links']['#cardinality'] = ElementMultiple::CARDINALITY_UNLIMITED;See Cardinality and unlimited.
#max_items
Section titled “#max_items”- Type: int
- Default:
1000
A hard cap on the number of rows. It applies even when #cardinality is unlimited. The element holds at most the smaller of #max_items and #cardinality rows, and the server enforces it on every request:
- A request that asks for more rows, such as a crafted
more_itemsvalue, is cut at this number. - A submission that posts more rows keeps the first rows, in the order they were posted, and drops the others before the element builds, validates or submits them.
- A
#default_valuewith more rows is cut too. The element renders and submits only the first rows, and the others are lost on the next save. The element logs a warning in theelement_multiplechannel when it happens, so the developer can raise the limit or pass fewer rows.
Lower the number when each row is expensive to render.
$form['rows']['#max_items'] = 200;#min_items
Section titled “#min_items”- Type: int or
NULL - Default:
NULL, which means0, or1when the element is#required
The minimum number of rows on screen. A row cannot be removed below this number.
$form['rows']['#min_items'] = 2;#empty_items
Section titled “#empty_items”- Type: int
- Default:
1
The number of blank rows added after the default values. Set it to 0 to start with only the default rows. When the default values already reach #min_items and a child is required, blank rows are not added, because they would fail validation.
$form['rows']['#empty_items'] = 0;See Min and empty items.
Messages
Section titled “Messages”#item_label
Section titled “#item_label”- Type: string
- Default:
item
The singular word used in the titles of the row buttons, for example Add new item after item 2 and Remove item 2.
$form['contacts']['#item_label'] = $this->t('contact');#no_items_message
Section titled “#no_items_message”- Type: string
- Default:
No items entered. Please add items below.
The message shown in place of the table when there are no rows. Set it to an empty string to show nothing.
$form['contacts']['#no_items_message'] = $this->t('No contacts yet.');Operations
Section titled “Operations”#sorting
Section titled “#sorting”- Type: bool
- Default:
TRUE
Adds a drag handle and a weight column. The weight input is hidden while JavaScript runs and can be shown with the “Show row weights” link. With FALSE the rows keep their order and no handle is rendered.
$form['rows']['#sorting'] = FALSE;See Sorting.
#operations
Section titled “#operations”- Type: bool
- Default:
TRUE
Shows the operations column with the per-row buttons. With FALSE the column is gone. The “Add more” button below the table is not affected. Use #add_more for that.
$form['rows']['#operations'] = FALSE;- Type: bool
- Default:
TRUE
Shows the per-row button that adds a blank row after that row. It is switched off by itself when #cardinality is reached.
$form['rows']['#add'] = FALSE;#remove
Section titled “#remove”- Type: bool
- Default:
TRUE
Shows the per-row button that removes that row.
$form['rows']['#remove'] = FALSE;Add more
Section titled “Add more”#add_more
Section titled “#add_more”- Type: bool
- Default:
TRUE
Shows the “Add more” button and its number input below the table. The block is also hidden when #cardinality is reached.
$form['rows']['#add_more'] = FALSE;#add_more_items
Section titled “#add_more_items”- Type: int
- Default:
1
The default number in the “Add more” input, which is how many rows one click adds. The number is cut by #cardinality and #max_items.
$form['rows']['#add_more_items'] = 3;#add_more_button_label
Section titled “#add_more_button_label”- Type: string
- Default:
Add
The label of the “Add more” button.
$form['rows']['#add_more_button_label'] = $this->t('Add rows');#add_more_input
Section titled “#add_more_input”- Type: bool
- Default:
TRUE
Shows the number input next to the button. With FALSE each click adds #add_more_items rows.
$form['rows']['#add_more_input'] = FALSE;#add_more_input_label
Section titled “#add_more_input_label”- Type: string
- Default:
more items
The text after the number input.
$form['rows']['#add_more_input_label'] = $this->t('more rows');Callbacks
Section titled “Callbacks”Use a static method or a function name. A closure cannot be stored in the form cache, so it fails when AJAX rebuilds the form.
#is_empty
Section titled “#is_empty”- Type: callable or
NULL - Default:
NULL
Decides whether a submitted row is empty. An empty row is dropped. The callback is called as callable(array|string $item, array $element): bool. It receives the item as it would be submitted, a string for a single element and an array for a composite one that includes the #key child, and the element. It returns TRUE for an empty row. Without it the module treats a row as empty when every value is an empty string or an empty array. That rule keeps a row with an unchecked checkbox, because 0 is not empty.
$form['tasks'] = [ '#type' => 'element_multiple', '#title' => $this->t('Tasks'), '#element' => [ 'label' => ['#type' => 'textfield', '#title' => $this->t('Label')], 'done' => ['#type' => 'checkbox', '#title' => $this->t('Done')], ], '#is_empty' => [static::class, 'isEmptyTask'],];
public static function isEmptyTask(array|string $item, array $element): bool { return trim((string) ($item['label'] ?? '')) === '';}#row_alter
Section titled “#row_alter”- Type: callable or
NULL - Default:
NULL
Called for each row after it is built, as callable(array &$row, int $index, mixed $item, array $element): void. It receives the row render array by reference, the row index, the item and the element. For a row that comes from #default_value the item is that default row, for a row the user added it is NULL. Use it to disable or decorate single rows. Keep the cells of the row, its weight and _operations_ cells and its _em_row_ input.
A disabled input of a row that comes from #default_value keeps the value of that default row on submit, even though a browser does not post it. For this, the rows of the element carry a token, as for children of type value. A child that is statically disabled in #element ('#disabled' => TRUE) needs no callback, the rows carry the token for it too, and it keeps the value of its own row when the rows are reordered.
When both #sorting and #operations are FALSE, the row has no handle and no operations cell, so the module puts the row token, and the hidden children, in the last cell. If that cell is a field, the module wraps it, and the field is under the key field of the cell: $row['note']['field']. Setting #disabled on the cell works, the module passes it to the field.
With a single element, or a composite one without #header, the inputs sit under the _item_ key of the row. With #header, each child is a key of the row.
$form['rows']['#row_alter'] = [static::class, 'lockFirstRow'];
public static function lockFirstRow(array &$row, int $index, mixed $item, array $element): void { if ($index === 0) { $row['_item_']['#disabled'] = TRUE; }}See Callbacks.
Markup
Section titled “Markup”#ajax_attributes
Section titled “#ajax_attributes”- Type: array
- Default:
[]
HTML attributes of the div that AJAX replaces. The id is set by the module.
$form['rows']['#ajax_attributes'] = ['class' => ['my-ajax-wrapper']];#table_attributes
Section titled “#table_attributes”- Type: array
- Default:
[]
HTML attributes of the table element.
$form['rows']['#table_attributes'] = ['class' => ['my-table'], 'data-test' => 'rows'];#table_wrapper_attributes
Section titled “#table_wrapper_attributes”- Type: array
- Default:
[]
HTML attributes of the div around the table. The module adds the class element-multiple-table.
$form['rows']['#table_wrapper_attributes'] = ['class' => ['my-table-wrapper']];See Custom attributes and labels and Theming.
Form API properties
Section titled “Form API properties”#required
Section titled “#required”#required_error sets the error message. With #required => TRUE at least one non-empty row must be submitted, and #min_items becomes 1. Without #required_error the message is <title> field is required.
A child with #required => TRUE is required in every row.
$form['rows']['#required'] = TRUE;$form['rows']['#required_error'] = $this->t('Add at least one row.');See Required.
#states
Section titled “#states”#states on the element shows, hides, requires or disables the whole list. On a child, a selector that uses the name of the element, such as :input[name="rows[done]"], is rewritten for each row to rows[items][0][done], so a child reacts to its siblings in the same row only.
'#states' => [ 'visible' => [':input[name="mode"]' => ['value' => 'advanced']],],#access
Section titled “#access”#access => FALSE on the element hides it and every child. A child with #access => FALSE is not rendered and its value is kept on the server.
$form['rows']['#access'] = $this->currentUser()->hasPermission('administer rows');Internal properties
Section titled “Internal properties”These are set by the element type. Change them only if you know why.
#input
Section titled “#input”Always TRUE. It tells the Form API that the element holds a value.
$form['rows']['#input'] = TRUE;#process
Section titled “#process”Holds ElementMultiple::processElementMultiple, which builds the table. To add your own step, append to the list so the module still runs first.
$form['rows']['#process'][] = [static::class, 'processRows'];#theme_wrappers
Section titled “#theme_wrappers”The wrapper around the element. It is a fieldset, which gives the group a legend.
$form['rows']['#theme_wrappers'] = ['fieldset'];#markup
Section titled “#markup”An empty string. It is kept for compatibility and has no use for you.
$form['rows']['#markup'] = '';