Drupal 7.x and 8.x

Turning forms into a table of information, such as can be seen on the users administration page (admin/people) or the content administration page (admin/content) is a much easier process in Drupal 7 than it was in Drupal 6. In Drupal 6 it required some tricky logic, combined with a theme function. Drupal 7 (the same in Drupal 8 as of this writing) removes the need for most of that.

Step 1: Get the Data

The first thing we need to do is get the data we will use in our table. This will be entirely dependent on what data it is that you want to show. The table I showed above is displaying user info from the database. There are various ways of getting this data, including a direct database call (though Drupal doesn't actually have first and last name columns). But in this case, I am just going to create an array with the example data. In a real situation, you would have to build this array from your database call or however else you are getting your data. Here is the example data that will be used for this tutorial:

$users = array(
  array('uid' => 1, 'first_name' => 'Indy', 'last_name' => 'Jones'),
  array('uid' => 2, 'first_name' => 'Darth', 'last_name' => 'Vader'),
  array('uid' => 3, 'first_name' => 'Super', 'last_name' => 'Man'),
);

As you can see, there are three rows. Each row is one user's data, that contains their UID, first name and last name. This simulates a database call that would grab this data for selected users.

Step 2: Build the Header

The next thing we need to do is put together an associative (keyed) array defining the header of the table. The keys here are important as we will be using later on in the tutorial. The table we are building in this tutorial has three cells in the header; one for the checkbox column, one for first name, and one for last name. However, we can ignore the cell for the checkbox column, as Drupal will take care of this for us later. As such, we can build our header as follows:

$header = array(
  'first_name' => t('First Name'),
  'last_name' => t('Last Name'),
);

Step 3: Build the Data

Next, we need to build the array that will contain the data of the table. Each element in the array will correspond to one row in the HTML table we are creating. Each element of the array will be given a unique ID. This will be the value of the checkbox/radios when the form is submitted (if selected). In this case, we want to get the UID of the user, so each row will be keyed with the UID of the user. We then will key the cells of the table with the keys as we used for the keys of the header. Again, we can ignore the checkboxes/radios as Drupal will take care of this for us later.

// Initialize an empty array
$options = array();
// Next, loop through the $users array
foreach ($users as $user) {
  // each element of the array is keyed with the UID
  $options[$user['uid']] = array(
    // 'first_name' was the key used in the header
    'first_name' => $user['first_name'],
    // 'last_Name' was the key used in the header 
    'last_name' => $user['last_name'], 
  );
}

If we were to analyze the $options array, we would see it looks like this:

(
  [1] => Array(
    [first_name] => Indy
    [last_name] => Jones
  )
  [2] => Array(
    [first_name] => Darth
    [last_name] => Vader
  )
  [3] => Array(
    [first_name] => Super
    [last_name] => Man
  )
)

Each element is keyed by the UID, and each element has two child elements, one for the first name and one for the last name.

Step 4: The Magic

So now we have built a header ($header), and the rows of the table ($options). All that is left is to bring it all together. Drupal 7 has a nice little theme function that we will use for this, theme_tableselect(). theme_tableselect() takes the data, turns it into a table, adds a checkbox to each row, and adds a 'select all' checkbox to the header. Handy! So lets look at how to tie this all together:

(
  '#type' => 'tableselect',
  '#header' => $header,
  '#options' => $options,
);

That's it. Really. This will do all the magic behind the scenes. We don't need to do use hook_theme, and we don't need to write any theme functions. This simple render element will take care of it all for us behind the scenes.

Additional Options

The above example will render the table with checkboxes. If we want to render the table with radios, we set #multiple to FALSE:

$form['table'] = array(
  '#type' => 'tableselect',
  '#header' => $header,
  '#options' => $options,
  '#multiple' => FALSE,
);

If for some reason, you don't want the select all checkbox added to the header, you can set #js_select to FALSE:

$form['table'] = array(
  '#type' => 'tableselect',
  '#header' => $header,
  '#options' => $options,
  '#js_select' => FALSE,
);

Retrieving the Selected Element

As the form element was set as $form['table'], in the submit function, the selected value (or values) can be retrieved at $form_state['values']['table']. Any unchecked items will be given a value of 0, checked items will be given a value of the item key.
We can use the array_filter function to give us only the selected items as in the example submit handler below, which will output the selected items as a Drupal message.

function my_form_submit($form , $form_state) {
  $results = array_filter($form_state['values']['table']);
  drupal_set_message(print_r($results , 1));
}

Summing it up Entire Code

Here an example form definition using the code described in this tutorial

function my_form($form, $form_state) {
  $users = array(
    array('uid' => 1, 'first_name' => 'Indy', 'last_name' => 'Jones'),
    array('uid' => 2, 'first_name' => 'Darth', 'last_name' => 'Vader'),
    array('uid' => 3, 'first_name' => 'Super', 'last_name' => 'Man'),
  );

  $header = array(
    'first_name' => t('First Name'),
    'last_name' => t('Last Name'),
  );
  $options = array();
  foreach ($users as $user) {
    $options[$user['uid']] = array(
      'first_name' => $user['first_name'],
      'last_name' => $user['last_name'],
    );
  }
  $form['table'] = array(
    '#type' => 'tableselect',
    '#header' => $header,
    '#options' => $options,
    '#empty' => t('No users found'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

Drupal 6.x

Occasionally you might need to display results to your users/visitors in a table and give them the option of saving the information displayed or performing some additional bulk action on that information (like on the /admin/user/user page or admin/content/node page). When views just can’t do it you may have to program the results. Here's an example of how you would do that.
This assumes you have already created a module and have results being displayed in a table or at least know how.

Step 1: Register a Theme function
If you haven’t used theme functions outside of following instructions to add code to template.php, don’t worry, adding and using a theme function is easier than you think:

 /**
  * Implementation of hook_theme().
  */
  function my_module_theme() {
    return array(
      'my_theme_function' => array(
      'arguments' => array(),
    ));
}

Note, there are a couple of options for naming your theme function. It can be named after the form function you intend to theme, or you can give it any name you want and then add it to the form using $form['#theme'] array. I've found using the name of the form function the easiest.

Step 2: Change your table row output to form fields:

Old example code:


  //Example function that gets user info from database
  function my_display_function() {
    //db query
    $sql = "SELECT * from users WHERE users.uid > 0";
    
    //set limit for pager
    $limit = 25;
    //define table header
    $header = array(
      array('data' => t('User ID'), 'field' => 'uid', 'sort' => 'asc'),
      array('data' => t('Username'), 'field' => 'name'),
      array('data' => t('Status'), 'field' => 'status'),
  );
    //alows sorting
    $tablesort = tablesort_sql($header);
    //adds a pager to results - show 25 per page
    $result = pager_query($sql.$tablesort, $limit);
    $rows = array();
    while($item = db_fetch_object($result)) {
      $rows[] = array($item->uid, l($item->name, 'user/'.$item->uid), $item->status);
    }
    
    $output .= theme('table', $header, $rows);
    $output .= theme('pager', NULL, $limit, 0);
    return $output;

  }


New Code:
Note, before adding new code, be sure to change all references to this function to drupal_get_form('my_display_function') as it now displays a form. If it was defined using the menu hook, be sure to change "page callback" to drupal_get_form and "page arguments" to this function name.

  //Example function that gets user info from database
  function my_display_function() {
    //db query
    $sql = "SELECT * from users WHERE users.uid > 0";
    
    //set limit for pager
    $limit = 25;
    //define table header
    $header = array(
      '', //note empty value, will use this later
      array('data' => t('User ID'), 'field' => 'uid', 'sort' => 'asc'),
      array('data' => t('Username'), 'field' => 'name'),
      array('data' => t('Status'), 'field' => 'status'),
  );
    //alows sorting
    $tablesort = tablesort_sql($header);
    //adds a pager to results - show 25 per page
    $result = pager_query($sql.$tablesort, $limit);
    $form = array();
    while($item = db_fetch_object($result)) {
       
      /*Add each user id to my checkboxes array.
      Only keys, no values */
      $checkboxes[$item->uid] = '';
      
      // You need unique keys for each user, so I use user id
      $form['uid'][$item->uid] = array(
        '#value' => $item->uid
      );
      $form['name'][$item->uid] = array(
        '#value' => l($item->name, 'user/'.$item->uid)
      );
     //
     $form['status'][$item->uid] = array(
        '#value' => $item->status
      );
      
    }
    
    $form['checkboxes'] = array('#type' => 'checkboxes', '#options' => $checkboxes);
    $form['pager'] = array('#value' => theme('pager', NULL, $limit, 0));
    
    return $form;

  }

Step 3: Theme the form

 /*
  * Theme form to display as a table
  */
  function theme_my_theme_function($form) {
    //define table header
    $header = array(
      theme('table_select_header_cell'), //using that previously empty field
      array('data' => t('User ID'), 'field' => 'uid', 'sort' => 'asc'),
      array('data' => t('Username'), 'field' => 'name'),
      array('data' => t('Status'), 'field' => 'status'),
    );
    if(!empty($form['checkboxes']['#options'])) {
      foreach (element_children($form['uid']) as $key) {
        $rows[] = array(
          drupal_render($form['checkboxes'][$key]),
          drupal_render($form['uid'][$key]),
          drupal_render($form['name'][$key]),
          drupal_render($form['status'][$key]),
       );
      }
    }
    else {
      $rows[] = array(array('data' => '<div class="error">No users found</div>', 'colspan' => 4));    
    }
    $output .= theme('table', $header, $rows);
    if ($form['pager']['#value']) {
      $output .= drupal_render($form['pager']);
    }

    $output .= drupal_render($form);
    return $output;

  }

And that should do it.
Key points to note: Whatever your theme function is defined as in your implementation of hook_theme, the actual function should start with theme_(then the function you defined)

Comments

vensires’s picture

In function theme_my_theme_function() I think that the if(!empty($form)) {...} statement is wrong. The reason is that $form will never be empty. Even if we assumed that only the result of the example code was stored in the $form variable, $form is not empty, as it contains $form['checkboxes'] = array().

Use this instead:

if(!empty($form['checkboxes']['#options']){
  //code goes here
} 
Zahor’s picture

You're right, as I was looking at your code I was thinking "but that's what I have" (or something to that effect) and then I looked at the code I added and realized that wasn't what I had. I noticed that the first time I implemented it and I got no results but also got no message saying I got no results but only an empty table.
Thanks for noticing.

lathan’s picture

added comment as a revision rather.

sapox’s picture

The 'page arguments' must be passed with an array:

'page arguments' => array('my_display_function');
spet’s picture

I have yet to find it documented, so here goes: The #default_values attribute works differently for tableselect than for other #types. The attribute can be used to preselect options and one would assume (or at least I did) that

$form['table'] = array (
  '#type' => 'tableselect',
  '#header' => $header,
  '#options' => $options,
  '#empty' => t('No users found'),
  '#default_value' => array(2),
);

would preselect Darth Vader in the above example. It does not. This does:

$form['table'] = array (
  '#type' => 'tableselect',
  '#header' => $header,
  '#options' => $options,
  '#empty' => t('No users found'),
  '#default_value' => array(2 => 1),
);

The issue #831966: Unable to uncheck rows in default_value. Tableselect needs value callback. notes it, but it will not change i D7.

puddyglum’s picture

Thanks for the heads-up... here is my implementation to make it work:

  foreach ($form['my_tableselect']['#default_value'] as $default_value)
  {
    if ($default_value)
    {
      $form['my_tableselect']['#default_value'][$default_value] = 1;
    }
  }
JohnHClark3’s picture

When radio buttons are required setting #multiple = FALSE will render them. The documentation says TRUE causes them to be rendered.

adityaj’s picture

The code shown in the example above always output "no user found"

firfin’s picture

Using the code above show me a table with the headers and just "No content available." below that. Using dpm() on $form does show the #options.

alamp’s picture

@ Zahor mexicoder, ronan.orb, drupalshrek, nohup. Thanks a lot!

I have put the example code above into the sandbox.
Even though I followed the guide, I have spent some hard time making it work.
I hope it saves someone some time.

git clone http://git.drupal.org/sandbox/alamp/2286657.git

or here

krystianbuczak’s picture

It is worth noting that the

#options array cannot have a 0 key, as it would not be possible to discern checked and unchecked states.

It took me a while to find what is wrong with the 0 key.

stomerfull’s picture

Hello ,
In Drupal 6, I am using tableselect theme with PagerSelect to generate table with checkboxes and pagination. Any thoughts on how to retain checkbox values on pagination?
Because in the first page , i checked 2 checkbox and go to the second page and check other checkbox

When i go back to the first page , the checked checkbox are lost

How can i fix it ?

Arvey18’s picture

where to put all the codes, im new in drupal. please help.

Murz’s picture

Why here '#title' option is not available, like in regular table?

jakegibs617’s picture

It took me a while to find this, but I think it should live here as well. I was trying to validate the tableselect form and https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21... is the best place to see how to do this.

dvmanjunath’s picture

In release https://www.drupal.org/project/drupal/releases/9.2.13 security patch added to form builder. after that field type "value" doesn't return any values in form submit function.
Not sure what changes needs to be done to the above form, if any one knows please add in comments.

For more details please check below links
Release page - https://www.drupal.org/project/drupal/releases/9.2.13
security page - https://www.drupal.org/sa-core-2022-003
changes can be viewed here Changes can be viewd here - https://github.com/drupal/drupal/commit/7401e2ade3602125b244b6cbe082d555....

Below Form which used to work , but after 9.2.13 doesn't work.

public function buildForm(array $form, FormStateInterface $form_state) {
    $options = [
      [
        'title' => 'How to Learn Drupal', 
        'content_type' => 'Article', 
        'status' => 'published', 
        'comment' => ['data'=> [
            '#type' => 'textfield', 
            '#title' => 'Comment for row1',
            '#title_display'=> 'invisible',
            '#name' => 'comment[1]',
            '#access' => TRUE,
            '#disabled' => FALSE,
          ],
        ],
      ], 
      [
        'title' => 'Privacy Policy', 
        'content_type' => 'Page', 
        'status' => 'published', 
        'comment' => ['data'=> [
            '#type' => 'textfield',
            '#title' => 'Comment for row2',
            '#title_display' => 'invisible',
            '#name' => 'comment[2]',
            '#access' => TRUE,
            '#disabled' => FALSE,
           ],
         ],
      ],
    ];
    $header = [
      'title' => 'Title',
      'content_type' => 'Content type',
      'status' => 'Status',
      'comment' => 'Comment',
    ];

    $form['tableselect_element'] = [
      '#type' => 'tableselect',
      '#header' => $header,
      '#options' => $options,
      '#empty' => 'No content available.',
    ];

    $form['comment'] = [
      '#type' => 'value',
    ];
    
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#name' => 'proceed',
      '#button_type' => 'primary',
      '#value' => $this->t('Proceed'),
      '#weight' => 10,
    ];

    return $form;
}

sadashiv’s picture

Hi All,

I was having the same problem, after debugging form_state etc I finally found a solution that we need to use $form_state->getUserInput() to get the textfield value.

Thanks,
Sadashiv