Skip to content

Callbacks

Both callbacks are optional. Use a static method, as [static::class, 'method'], or a function name. A closure cannot be stored in the form cache, so it fails when AJAX rebuilds the form.

The module drops empty rows from the value. A row that holds an unchecked checkbox is not empty by the default rule, because the checkbox value is 0. #is_empty receives the item and returns TRUE when the row must be dropped.

$form['tasks'] = [
'#type' => 'element_multiple',
'#title' => $this->t('Tasks'),
'#header' => TRUE,
'#element' => [
'label' => [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#title_display' => 'invisible',
'#placeholder' => $this->t('Enter label'),
],
'done' => [
'#type' => 'checkbox',
'#title' => $this->t('Done'),
],
],
'#default_value' => [
['label' => 'Write the docs', 'done' => 1],
['label' => 'Review the docs', 'done' => 0],
],
'#is_empty' => [static::class, 'isEmptyTask'],
];
public static function isEmptyTask(array|string $item, array $element): bool {
return trim((string) ($item['label'] ?? '')) === '';
}

A table of tasks with two filled rows and a blank last row. The blank row has an empty label and an unchecked box, and it is dropped on submit.

#row_alter is called for each built row. It receives the row by reference, the index of the row and the item. Use it to lock one row. This second list has the same children and tasks as the first one.

$form['locked_tasks']['#row_alter'] = [static::class, 'lockFirstRow'];
public static function lockFirstRow(array &$row, int $index, mixed $item, array $element): void {
if ($index === 0) {
$row['label']['#disabled'] = TRUE;
}
}

With #header, each child is a key of the row, as in this example. With a single element, or a composite without #header, the inputs are under _item_.

A table of tasks. The label of the first row is disabled and grayed out, and the other rows can be edited.

  • A disabled field is not sent by the browser. For a row that comes from #default_value, the module gives the disabled field the value of that default row on submit. A row the user added has no default row, so a disabled field of it comes back empty.
  • A child that is disabled in #element, with no callback, behaves the same: it keeps the value of its own row, also after the rows are reordered.
  • With #sorting and #operations both FALSE, the last cell of the row is wrapped to hold the row token. The field is then under $row['done']['field'], and #disabled on the cell reaches the field.
  • If the #default_value changed between the moment the form was shown and the submission, the form fails validation with “The form is out of date. Reload the page and try again.” The rows no longer match the defaults the server keeps values for. See Hidden and server-only children.
  • Related properties: #is_empty, #row_alter.