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

/**
 * 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

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.
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.net.br

Fonski’s picture

When I try this example, the table lines are not ordered by the defined weight.
Does someone has any idea ?

Don't know how, but it works now. Thanks for the example !

Jamie Holly’s picture

For anyone doing more advanced type tabledrag forms. 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.

---------------------

HollyIT - Grab the Netbeans Drupal Development Tool at GitHub.

youfei.sun’s picture

Thanks Jamie, but can you please explain more how this procedure works? I tried to read the two files there, but I had no luck there.

alexdmccabe’s picture

This doesn't seem to be related to the table being draggable: https://www.drupal.org/node/2703941

tommycox’s picture

Note: You only want to use one of both, either #tabledrag or #tableselect in a single table, not both.

But what if I DO want to use both, say to apply weights (TableDrag) AND to enable/disable an item (TableSelect) on the same form?

"Aut inveniam viam aut faciam" - I shall either find a way or make one.

tommycox’s picture

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.

"Aut inveniam viam aut faciam" - I shall either find a way or make one.

NancyDru’s picture

Before I read that, I had created a table with both (in 7.x) using the Elements module, and it seems to work. I can't, for the life of me, figure out how you know what rows were selected.

DiDebru’s picture

With this example I dont have a table weight column I needed to attach drupal.tabledrag but still the table is not sortable.