Form Element 'table' with TableDrag and TableSelect
Building and rendering a table required writing a custom/dedicated theme function for most tables in Drupal 7.
Drupal 8 introduces a new element '#type' => 'table'
, which comes with built-in, optional support for TableDrag and TableSelect, and which requires no more custom plumbing for most use cases.
Note: The following conversion example shows both TableDrag and TableSelect in a single table for brevity, but they are usually not combined in a single table.
Example
use Drupal\Component\Utility\String;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Form constructor for the administrative listing/overview form.
*/
function mymodule_admin_overview_form($form, FormStateInterface $form_state) {
$form['mytable'] = array(
'#type' => 'table',
'#header' => array(t('Label'), t('Machine name'), t('Weight'), t('Operations')),
'#empty' => t('There are no items yet. <a href="@add-url">Add an item.</a>', array(
'@add-url' => Url::fromRoute('mymodule.manage_add'),
)),
// TableSelect: Injects a first column containing the selection widget into
// each table row.
// Note that you also need to set #tableselect on each form submit button
// that relies on non-empty selection values (see below).
'#tableselect' => TRUE,
// TableDrag: Each array value is a list of callback arguments for
// drupal_add_tabledrag(). The #id of the table is automatically prepended;
// if there is none, an HTML ID is auto-generated.
'#tabledrag' => array(
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'mytable-order-weight',
),
),
);
// Build the table rows and columns.
// The first nested level in the render array forms the table row, on which you
// likely want to set #attributes and #weight.
// Each child element on the second level represents a table column cell in the
// respective table row, which are render elements on their own. For single
// output elements, use the table cell itself for the render element. If a cell
// should contain multiple elements, simply use nested sub-keys to build the
// render element structure for drupal_render() as you would everywhere else.
foreach ($entities as $id => $entity) {
// TableDrag: Mark the table row as draggable.
$form['mytable'][$id]['#attributes']['class'][] = 'draggable';
// TableDrag: Sort the table row according to its existing/configured weight.
$form['mytable'][$id]['#weight'] = $entity->get('weight');
// Some table columns containing raw markup.
$form['mytable'][$id]['label'] = array(
'#plain_text' => $entity->label(),
);
$form['mytable'][$id]['id'] = array(
'#plain_text' => $entity->id(),
);
// TableDrag: Weight column element.
// NOTE: The tabledrag javascript puts the drag handles inside the first column,
// then hides the weight column. This means that tabledrag handle will not show
// if the weight element will be in the first column so place it further as in this example.
$form['mytable'][$id]['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight for @title', array('@title' => $entity->label())),
'#title_display' => 'invisible',
'#default_value' => $entity->get('weight'),
// Classify the weight element for #tabledrag.
'#attributes' => array('class' => array('mytable-order-weight')),
);
// Operations (dropbutton) column.
$form['mytable'][$id]['operations'] = array(
'#type' => 'operations',
'#links' => array(),
);
$form['mytable'][$id]['operations']['#links']['edit'] = array(
'title' => t('Edit'),
'url' => Url::fromRoute('mymodule.manage_edit', array('id' => $id)),
);
$form['mytable'][$id]['operations']['#links']['delete'] = array(
'title' => t('Delete'),
'url' => Url::fromRoute('mymodule.manage_delete', array('id' => $id)),
);
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save changes'),
// TableSelect: Enable the built-in form validation for #tableselect for
// this form button, so as to ensure that the bulk operations form cannot
// be submitted without any selected items.
'#tableselect' => TRUE,
);
return $form;
}
Note: You only want to use one of both, either #tabledrag or #tableselect in a single table, not both. This example only contains both for brevity.
In short:
- The render array structure defines the whole table.
- The corresponding theme function, form validation handler, and hook_theme() entry can be removed.
- Use #tabledrag to attach a TableDrag behavior for table rows to allow sorting.
- Use #tableselect to inject a TableSelect form widget as first column in all table rows.
Developers Remarks
- It's best to add the table drag element in a theme function (see /core/lib/Drupa/Core/Field/WidgetBase.php and /core/include/theme.inc for an example.) The problem is that adding certain elements into a draggable table in the form build can cause validation errors. An example of this is a DateTime field.
- You can use only one, not both, either #tabledrag or #tableselect in a single table, not both.
- A workaround I've come to was to add a checkbox element for each row while using TableDrag. There's no Select All behavior but that could possibly be implemented without much effort on the front-end.
Verified Example
$pass_list = $this->manager->selectIPGroups('pass');
foreach ($pass_list as $id=>$data) {
$form['pass_table'][$id]['#attributes']['class'][] = 'draggable';
$form['pass_table'][$id]['#weight'] = $id;
$form['pass_table'][$id]['id'] = array('#plain_text' => $data->id, );
$form['pass_table'][$id]['group ip'] = array('#plain_text' => $data->ip_group, );
$form['pass_table'][$id]['expire'] = array('#plain_text' => $data->expire, );
$form['pass_table'][$id]['active'] = array('#plain_text' => $data->active, );
$form['pass_table'][$id]['operate'] = array(
'#type' => 'operations',
'#links' => array(),
);
$title_active = ($data->active==1)? t('disable') : t('enable');
$form['pass_table'][$id]['operate']['#links']['active'] = array(
'title' => $title_active,
'url' => Url::fromRoute('drupalvip_sentry.active', array('ipgroup_id' => $data->id)),
);
$form['pass_table'][$id]['operate']['#links']['delete'] = array(
'title' => t('delete'),
'url' => Url::fromRoute('drupalvip_sentry.delete', array('ipgroup_id' => $data->id)),
);
}
Form Result
as you can see there is a draggable icon per each row, which enables you to do the dragging operation, of course, you need to save the new weight in the list.
This method gives you a check-all operation and you can support it from the submit method.
See the operation drop-down select options
Note: You only want to use one of both, either #tabledrag or #tableselect in a single table, not both. This example only contains both for brevity.
In short:
- The render array structure defines the whole table.
- The corresponding theme function, form validation handler, and hook_theme() entry can be removed.
- Use #tabledrag to attach a TableDrag behavior for table rows to allow sorting.
- Use #tableselect to inject a TableSelect form widget as the first column in all table rows.
Tableselect: Getting the selected rows on submit
using the 'tableselect' attribute, create a checkbox per each row
now we can select specific rows or all rows
on submission, we want to know which rows were selected.
The return value is an array of keys of all selected rows or null if none are selected.
The getValue of the table returns a list of keys: $form_state->getValue('our_table')
so we first check if it is null. if it's not null then we have a list of keys that we can go over it.
if you create a table with a specific id that you get from your archive make sure to build your table, so the key will be the ID you intend to use
creating the table rows with our id as key:
$id = $item['id'];
$form['our_table'][$id]['#weight'] = $id;
$form['our_table'][$id]['id'] = array('#plain_text' => $item['id'], );
getting the table value on submission:
public function submit(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('our_table') != NULL) {
foreach ($form_state->getValue('our_table') as $key => $id) {
if ($ipgroup != 0) {
// collect all selected into simple array
$selected[] = $id;
// do something according it key/id
}
}
}
}