Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

Building and rendering a table required to write a custom/dedicated theme function for most tables in Drupal 7. TableSelect (bulk operations) and TableDrag (drag & drop) support had to be added and attached manually when necessary. This led to many duplicate/unnecessary theme functions, which in turn made it hard for other modules to enhance or alter a table.

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.

Drupal 7

<?php
/**
 * Form constructor for the administrative listing/overview form.
 */
function mymodule_admin_overview_form($form, &$form_state) {
 
$form['mytable'] = array(
   
'#tree' => TRUE,
   
// Note: For TableSelect instead of TableDrag, you would have specified
    //   'tableselect' as render element #type instead of this #theme.
   
'#theme' => 'mymodule_admin_overview_form_table',
  );
  foreach (
$entities as $id => $entity) {
   
// Some table columns.
   
$form['mytable'][$id]['title'] = $entity->title;
   
$form['mytable'][$id]['name'] = $entity->name;

   
// Prepare for TableDrag support.
   
$form['mytable'][$id]['weight'] = array(
     
'#type' => 'weight',
     
'#title' => t('Weight for @title', array('@title' => $entity->title)),
     
'#title_display' => 'invisible',
     
'#default_value' => $entity->weight,
    );

   
// Operation columns.
   
$form['mytable'][$id]['edit'] = array(
     
'#type' => 'link',
     
'#title' => t('edit'),
     
'#href' => "admin/config/mymodule/$id",
    );
   
$form['mytable'][$id]['delete'] = array(
     
'#type' => 'link',
     
'#title' => t('delete'),
     
'#href' => "admin/config/mymodule/$id/delete",
    );
  }
 
$form['actions'] = array('#type' => 'actions');
 
$form['actions']['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Save changes'),
  );
  return
$form;
}

/**
 * Form validation handler for mymodule_admin_overview_form().
 *
 * Note: Only applies to TableSelect (bulk operation) forms, not TableDrag.
 */
function mymodule_admin_overview_form_validate($form, &$form_state) {
 
// Throw an error in case no items have been selected.
 
if (!is_array($form_state['values']['mytable']) || !count(array_filter($form_state['values']['mytable']))) {
   
form_set_error('', t('No items selected.'));
  }
}

/**
 * Implements hook_theme().
 */
function mymodule_theme() {
 
$theme['mymodule_admin_overview_form_table'] = array(
   
'render element' => 'element',
   
'file' => 'mymodule.admin.inc',
  );
  return
$theme;
}

/**
 * Returns HTML for the table in the administrative listing/overview form.
 *
 * @param $variables
 *   An associative array containing:
 *   - element: A render element representing the table in the form.
 *
 * @ingroup themeable
 */
function theme_mymodule_admin_overview_form_table($variables) {
 
$element = $variables['element'];

 
$rows = array();
  foreach (
element_children($element) as $id) {
   
// Classify the weight element for TableDrag.
   
$element[$id]['weight']['#attributes']['class'] = array('mytable-order-weight');

   
// Mark the table row as draggable for TableDrag.
   
$row = array(
     
'data' => array(),
     
'class' => array('draggable'),
    );
   
// Render the table columns.
   
$row['data'][] = drupal_render($element[$id]['title']);
   
$row['data'][] = drupal_render($element[$id]['name']);
   
$row['data'][] = drupal_render($element[$id]['weight']);
   
$row['data'][] = drupal_render($element[$id]['edit']);
   
$row['data'][] = drupal_render($element[$id]['delete']);

   
$rows[] = $row;
  }

 
// Build the table header.
 
$header = array(
   
t('Title'),
   
t('Machine name'),
   
t('Weight'),
    array(
'data' => t('Operations'), 'colspan' => 2),
  );
 
// Render the table.
  // Note: For TableSelect instead of TableDrag, you would have specified
  //   'tableselect' as render element #type and passed the $rows as 'options'
  //   instead of 'rows'.
 
$output = theme('table', array(
   
'header' => $header,
   
'rows' => $rows,
   
'attributes' => array('id' => 'mytable-order'),
  ));
 
$output .= drupal_render_children($element);

 
// Attach TableDrag to the table ID and contained weight elements.
 
drupal_add_tabledrag('mytable-order', 'order', 'sibling', 'mytable-order-weight');

  return
$output;
}
?>

Drupal 8

<?php
use Drupal\Component\Utility\String;
use
Drupal\Core\Form\FormStateInterface;

/**
 * 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('admin/config/mymodule/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(
     
'#markup' => String::checkPlain($entity->label()),
    );
   
$form['mytable'][$id]['id'] = array(
     
'#markup' => String::checkPlain($entity->id()),
    );

   
// TableDrag: Weight column element.
   
$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'),
     
'href' => "admin/config/mymodule/manage/$id",
    );
   
$form['mytable'][$id]['operations']['#links']['delete'] = array(
     
'title' => t('Delete'),
     
'href' => "admin/config/mymodule/manage/$id/delete",
    );
  }
 
$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.
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done

Comments

yukare’s picture

Please, can someone add a example how to use this with #tabledrag and multilevel?

Fernando Correa da Conceição
http://jaguaribe.org