Skip to content

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.

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

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

  • Type: TRUE, FALSE, string, array or NULL
  • 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%'],
];

See Composite with header.

  • 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');
  • 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.

  • 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_items value, 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_value with 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 the element_multiple channel 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;
  • Type: int or NULL
  • Default: NULL, which means 0, or 1 when the element is #required

The minimum number of rows on screen. A row cannot be removed below this number.

$form['rows']['#min_items'] = 2;
  • 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.

  • 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');
  • 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.');
  • 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.

  • 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;
  • Type: bool
  • Default: TRUE

Shows the per-row button that removes that row.

$form['rows']['#remove'] = FALSE;

See Operations and 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;
  • 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;
  • Type: string
  • Default: Add

The label of the “Add more” button.

$form['rows']['#add_more_button_label'] = $this->t('Add rows');
  • 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;
  • Type: string
  • Default: more items

The text after the number input.

$form['rows']['#add_more_input_label'] = $this->t('more rows');

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.

  • 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'] ?? '')) === '';
}
  • 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.

  • 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']];
  • Type: array
  • Default: []

HTML attributes of the table element.

$form['rows']['#table_attributes'] = ['class' => ['my-table'], 'data-test' => 'rows'];
  • 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.

#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 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']],
],

See States between siblings.

#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');

These are set by the element type. Change them only if you know why.

Always TRUE. It tells the Form API that the element holds a value.

$form['rows']['#input'] = TRUE;

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'];

The wrapper around the element. It is a fieldset, which gives the group a legend.

$form['rows']['#theme_wrappers'] = ['fieldset'];

An empty string. It is kept for compatibility and has no use for you.

$form['rows']['#markup'] = '';