Drupal for Beginners: How to Create a Table Using theme_table()

A friend asked me recently how to create a table in Drupal in a proper way.

Note that examples below are for Drupal 6, as there are differences in implementation of theme_table() function between versions 6 and 7. For more information on this and example of a change required to make them work in D7 see the end of this post.

A friend asked me recently how to create a table in Drupal in a proper way.

Let's start with the very basic version - simple header and few rows, only data without any extra attributes.

To do this we need to create two arrays (one for header, and one for data), and then pass them to theme_table() function:

// Table header
$header = array(
  'fruit' => t('Fruit'),
  'a'     => t('Vitamin A'),
  'b1'    => t('Vitamin B1'),
  'b2'    => t('Vitamin B2'),
);

// Table rows
$rows = array(
  // Row 1
  array(
    'fruit' => t('Apple'),
    'a'     => '98 IU',
    'b1'    => '0.031 mg',
    'b2'    => '0.047 mg',
  ),
  // Row 2
  array(
    'fruit' => t('Banana'),
    'a'     => '76 IU',
    'b1'    => '0.037 mg',
    'b2'    => '0.086 mg',
  ),
  // Row 3
  array(
    'fruit' => t('Orange'),
    'a'     => '295 IU',
    'b1'    => '0.114 mg',
    'b2'    => '0.052 mg',
  ),
);

// Format and print out table.
print theme('table', $header, $rows);

The result:

<table> 
  <thead>
    <tr>
      <th>Fruit</th>
      <th>Vitamin A</th>
      <th>Vitamin B1</th>
      <th>Vitamin B2</th>
    </tr>
  </thead> 
  <tbody> 
    <tr class="odd">
      <td>Apple</td>
      <td>98 IU</td>
      <td>0.031 mg</td>
      <td>0.047 mg</td>
    </tr> 
    <tr class="even">
      <td>Banana</td>
      <td>76 IU</td>
      <td>0.037 mg</td>
      <td>0.086 mg</td>
    </tr> 
    <tr class="odd">
      <td>Orange</td>
      <td>295 IU</td>
      <td>0.114 mg</td>
      <td>0.052 mg</td>
    </tr> 
  </tbody> 
</table>

Now let's assume that we want to add some classes and ids to our table cells. To do this first we need to move the data one level deeper, into nested arrays with keys labelled data, and then define additional attributes on the same level as data arrays:

  // Table header
  $header = array(
    // Column 1
    'fruit' => array(
      'data'  => t('Fruit'),
      'id'    => 'head-fruit',
      'class' => 'col-fruit',
    ),
    // Column 2
    'a' => array(
      'data'  => t('Vitamin A'),
      'id'    => 'head-vit-a',
      'class' => 'col-vit-a',
    ),
    // Column 3
    'b1' => array(
      'data'  => t('Vitamin B1'),
      'id'    => 'head-vit-b1',
      'class' => 'col-vit-b1',
    ),
    // Column 4
    'b2' => array(
      'data'  => t('Vitamin B2'),
      'id'    => 'head-vit-b2',
      'class' => 'col-vit-b2',
    ),
  );

  // Table rows
  $rows = array(
    // Row 1
    array(
      // Column 1
      'fruit' => array(
        'data'  => t('Apple'),
        'id'    => 'row1-fruit',
        'class' => 'col-fruit',
      ),
      // Column 2
      'a' => array(
        'data'  => '98 IU',
        'id'    => 'row1-vit-a',
        'class' => 'col-vit-a',
      ),
      // Column 3
      'b1' => array(
        'data'  => '0.031 mg',
        'id'    => 'row1-vit-b1',
        'class' => 'col-vit-b1',
      ),
      // Column 4
      'b2' => array(
        'data'  => '0.047 mg',
        'id'    => 'row1-vit-b2',
        'class' => 'col-vit-b2',
      ),
    ),
    // Row 2
    array(
      // Column 1
      'fruit' => array(
        'data'  => t('Banana'),
        'id'    => 'row2-fruit',
        'class' => 'col-fruit',
      ),
      // Column 2
      'a' => array(
        'data'  => '76 IU',
        'id'    => 'row2-vit-a',
        'class' => 'col-vit-a',
      ),
      // Column 3
      'b1' => array(
        'data'  => '0.037 mg',
        'id'    => 'row2-vit-b1',
        'class' => 'col-vit-b1',
      ),
      // Column 4
      'b2' => array(
        'data'  => '0.086 mg',
        'id'    => 'row2-vit-b2',
        'class' => 'col-vit-b2',
      ),
    ),
    // Row 3
    array(
      // Column 1
      'fruit' => array(
        'data'  => t('Orange'),
        'id'    => 'row3-fruit',
        'class' => 'col-fruit',
      ),
      // Column 2
      'a' => array(
        'data'  => '295 IU',
        'id'    => 'row3-vit-a',
        'class' => 'col-vit-a',
      ),
      // Column 3
      'b1' => array(
        'data'  => '0.114 mg',
        'id'    => 'row3-vit-b1',
        'class' => 'col-vit-b1',
      ),
      // Column 4
      'b2' => array(
        'data'  => '0.052 mg',
        'id'    => 'row3-vit-b2',
        'class' => 'col-vit-b2',
      ),
    ),
  );

  // Format and print out the table.
  return theme('table', $header, $rows, array(
    'id'    => 'fruit-table',
    'class' => 'my-table',
  ));

Result looks the same as above, but if you view its source, you will see all new ids and classes added there:

<table id="fruit-table" class="my-table"> 
 <thead>
   <tr>
     <th id="head-fruit" class="col-fruit">Fruit</th>
     <th id="head-vit-a" class="col-vit-a">Vitamin A</th>
     <th id="head-vit-b1" class="col-vit-b1">Vitamin B1</th>
     <th id="head-vit-b2" class="col-vit-b2">Vitamin B2</th>
   </tr>
 </thead> 
 <tbody> 
   <tr class="odd">
     <td id="row1-fruit" class="col-fruit">Apple</td>
     <td id="row1-vit-a" class="col-vit-a">98 IU</td>
     <td id="row1-vit-b1" class="col-vit-b1">0.031 mg</td>
     <td id="row1-vit-b2" class="col-vit-b2">0.047 mg</td>
   </tr> 
   <tr class="even">
     <td id="row2-fruit" class="col-fruit">Banana</td>
     <td id="row2-vit-a" class="col-vit-a">76 IU</td>
     <td id="row2-vit-b1" class="col-vit-b1">0.037 mg</td>
     <td id="row2-vit-b2" class="col-vit-b2">0.086 mg</td>
   </tr> 
   <tr class="odd">
     <td id="row3-fruit" class="col-fruit">Orange</td>
     <td id="row3-vit-a" class="col-vit-a">295 IU</td>
     <td id="row3-vit-b1" class="col-vit-b1">0.114 mg</td>
     <td id="row3-vit-b2" class="col-vit-b2">0.052 mg</td>
   </tr> 
  </tbody> 
</table>

The same trick could be applied to table rows as well. To add additional attributes for each table row, we need to move all data of each row one level deeper (the same way as it was done for table cells before) into data keys, and add extra keys for new attributes (ids, classes, styles etc).

Let's also combine it with sample colspans and rowspans in the next example (note that because header has only one row by definition, it cannot be nested to "data" key as a whole, so I have removed it from this example, as it needs to stay the same as in previous one):

  // Table rows
  $rows = array(
    // Row 1
    array(
      'data' => array(
        // Column 1
        'fruit' => array(
          'data'    => t('Apple'),
          'id'      => 'row1-fruit',
          'class'   => 'col-fruit',
        ),
        // Column 2
        'a' => array(
          'data'    => '98 IU',
          'id'      => 'row1-vit-a',
          'class'   => 'col-vit-a',
          'align'   => 'right',
        ),
        // Column 3
        'b1' => array(
          'data'    => '0.031 mg',
          'id'      => 'row1-vit-b1',
          'class'   => 'col-vit-b1',
        ),
        // Column 4
        'b2' => array(
          'data'    => '0.047 mg',
          'id'      => 'row1-vit-b2',
          'class'   => 'col-vit-b2',
          'rowspan' => 3,
        ),
      ),
      // Row 1 attributes
      'id'    => 'row1',
      'class' => 'table-row',
    ),
    // Row 2
    array(
      'data' => array(
        // Column 1
        'fruit' => array(
          'data'    => t('Banana'),
          'id'      => 'row2-fruit',
          'class'   => 'col-fruit',
          'style'   => 'color: #f00'
        ),
        // Column 2
        'a' => array(
          'data'    => '76 IU',
          'id'      => 'row2-vit-a',
          'class'   => 'col-vit-a',
          'colspan' => 2,
          'align'   => 'center',
          'bgcolor' => '#ffccff',
        ),
        // Column 3
        // Removed due to colspan in column 2.
        // Column 4
        // Removed due to rowspan in row 1.
      ),
      // Row 2 attributes
      'id'    => 'row2',
      'class' => 'table-row',
    ),
    // Row 3
    array(
      'data' => array(
        // Column 1
        'fruit' => array(
          'data'    => t('Orange'),
          'id'      => 'row3-fruit',
          'class'   => 'col-fruit',
        ),
        // Column 2
        'a' => array(
          'data'    => '295 IU',
          'id'      => 'row3-vit-a',
          'class'   => 'col-vit-a',
          'align'   => 'right',
          'style'   => 'font-weight: bold',
        ),
        // Column 3
        'b1' => array(
          'data'    => '0.114 mg',
          'id'      => 'row3-vit-b1',
          'class'   => 'col-vit-b1',
          'style'   => 'font-style: italic',
        ),
        // Column 4
        // Removed due to rowspan in row 1.
      ),
      // Row 3 attributes
      'id'    => 'row3',
      'class' => 'table-row',
    ),
  );

  // Format and print out the table.
  return theme('table', $header, $rows, array(
    'id'      => 'fruit-table',
    'class'   => 'my-table',
    'border'  => 2,
  ));

And the result:

<table id="fruit-table" class="my-table" border="2"> 
  <thead>
    <tr>
      <th id="head-fruit" class="col-fruit">Fruit</th>
      <th id="head-vit-a" class="col-vit-a">Vitamin A</th>
      <th id="head-vit-b1" class="col-vit-b1">Vitamin B1</th>
      <th id="head-vit-b2" class="col-vit-b2">Vitamin B2</th>
    </tr>
  </thead> 
  <tbody> 
    <tr id="row1" class="table-row odd">
      <td id="row1-fruit" class="col-fruit">Apple</td>
      <td id="row1-vit-a" class="col-vit-a" align="right">98 IU</td>
      <td id="row1-vit-b1" class="col-vit-b1">0.031 mg</td>
      <td id="row1-vit-b2" class="col-vit-b2" rowspan="3">0.047 mg</td>
    </tr> 
    <tr id="row2" class="table-row even">
      <td id="row2-fruit" class="col-fruit" style="color: #f00">Banana</td>
      <td id="row2-vit-a" class="col-vit-a" colspan="2" align="center" bgcolor="#ffccff">76 IU</td>
    </tr> 
    <tr id="row3" class="table-row odd">
      <td id="row3-fruit" class="col-fruit">Orange</td>
      <td id="row3-vit-a" class="col-vit-a" align="right" style="font-weight: bold">295 IU</td>
      <td id="row3-vit-b1" class="col-vit-b1" style="font-style: italic">0.114 mg</td>
    </tr> 
  </tbody> 
</table>

Finally, let's combine all above with fetching data from database:

  // Table header
  $table_header = array(
    // Column 1
    'nid'   => array(
      'data'  => 'Nid',
      'class' => 'col-nid',
    ),
    // Column 2
    'title' => array(
      'data'  => t('Title'),
      'class' => 'col-title',
    ),
    // Column 3
    'date'  => array(
      'data'  => t('Date'),
      'class' => 'col-date',
    ),
  );

  // Table rows
  $sql = "SELECT n.nid, n.title, n.created FROM {node} n WHERE n.type = 'article' LIMIT 5";
  $res = db_query($sql);
  $table_rows = array();
  while ($row = db_fetch_object($res)) {
    $table_rows[] = array(
      'data' => array(
        // Column 1
        'nid'   => array(
          'data'  => $row->nid,
          'class' => 'col-nid',
        ),
        // Column 2
        'title' => array(
          'data'  => l($row->title, 'node/' . $row->nid),
          'class' => 'col-title',
        ),
        // Column 3
        'date'  => array(
          'data'  => format_date($row->created, 'short'),
          'class' => 'col-date',
        ),
      ),
      // Row attributes
      'id'    => 'articles-row-' . $row->nid,
      'class' => 'articles-row',
    );
  }

  // Format and print out the table.
  return theme('table', $table_header, $table_rows, array(
    'id'      => 'table-articles',
    'class'   => 'articles',
  ));

This gives us following result:

<table id="table-articles" class="articles"> 
  <thead>
    <tr>
      <th class="col-nid">Nid</th>
      <th class="col-title">Title</th>
      <th class="col-date">Date</th>
    </tr>
  </thead> 
  <tbody> 
    <tr id="articles-row-4" class="articles-row odd">
      <td class="col-nid">4</td>
      <td class="col-title"><a href="/node/4">Benchmarking Node.js - basic performance tests against Apache + PHP</a></td>
      <td class="col-date">Thu, 08/19/2010 - 17:45</td>
    </tr> 
    <tr id="articles-row-6" class="articles-row even">
      <td class="col-nid">6</td>
      <td class="col-title"><a href="/node/6">node.js + Socket.IO: Cannot Connect to Web Socket Server (SecurityError)</a></td>
      <td class="col-date">Sat, 08/14/2010 - 22:58</td>
    </tr> 
    <tr id="articles-row-7" class="articles-row odd">
      <td class="col-nid">7</td>
      <td class="col-title"><a href="/node/7">Getting Started with Node.js: Installation and Resources</a></td>
      <td class="col-date">Thu, 08/12/2010 - 18:44</td>
    </tr> 
    <tr id="articles-row-8" class="articles-row even">
      <td class="col-nid">8</td>
      <td class="col-title"><a href="/node/8">A Style Guide for PHP Developers</a></td>
      <td class="col-date">Tue, 08/10/2010 - 18:08</td>
    </tr> 
    <tr id="articles-row-9" class="articles-row odd">
      <td class="col-nid">9</td>
      <td class="col-title"><a href="/node/9">JavaScript: The World&#039;s Most Misunderstood Programming Language</a></td>
      <td class="col-date">Sun, 08/08/2010 - 16:54</td>
    </tr> 
  </tbody> 
</table>

There are many other things which could be added here, like for sorting or pager to name only few, but let's leave them for another, a little bit more advanced tutorial.

Drupal 7

It works in exactly the same way for Drupal 7.x, the only difference is a way of passing parameters to theme_table() function - instead of two separate parameters they are being passed as one associative array. For more details see Drupal API documentation.

The only difference in implementing the table between Drupal 6 and 7 is the way parameters are passed to theme_table(). In Drupal 6 this function is defined as:

theme_table($header, $rows, $attributes = array(), $caption = NULL)

while in Drupal 7:

theme_table($variables)

So in this case our call to theme_table() function would look like this:

  // Format and print out the table.
  return theme('table', array(
    'header'  => $table_header,
    'rows'    => $table_rows,
    'id'      => 'table-articles',
    'class'   => 'articles',
  ));