TAPIr (Tables API)

During the Ubercart 1.x development cycle, TAPIr was developed as a contributed module and released on drupal.org at http://drupal.org/project/tapir. One of the goals of Ubercart 2.x was to reduce the number of dependencies, so TAPIr was rolled into core and trimmed down. The result is a system that piggybacks on the Forms API, allowing forms to integrate with TAPIr much more naturally and tables to be altered through the common hook_form_alter(). The user interface was trimmed down, but the API itself was made more robust and easily altered. Table of contents:

Brief description:

The basic idea behind TAPIr is that you use the Forms API syntax to add a table array to $form as a tapir_table element. The very basic declaration for the cart view table form element is:
<?php
 
// Example from uc_cart.module function uc_cart_view_table().
 
$form['items'] = array(
   
'#type' => 'tapir_table',
   
'#tree' => TRUE,
  );

  return
$form;
?>
The theme function will take the data in #columns and its form element children to build table $header and $rows arrays that get passed on to Drupal's core theme_table() function. In addition to this data, TAPIr will pass #attributes as $attributes and #title as $caption to theme_table(). For more information on what's expected in these various arguments, see the documentation for theme_table().

Table data structure:

The table header is built from the array you should set as #columns for the table element. The array is specified here so it does not interfere with the Forms API rendering process which will attempt to render any element of a form array whose key does not start with #. The header is generated from an array of column data you include in the table array. The #columns array should use a unique string ID for the key of each column and an array of column data for the value. Column data includes a weight used for ordering, a TRUE or FALSE access value to determine the display of the whole table column, and cell data that will actually be used in the header array. Two code examples should make this a little clearer:
<?php
 
// Example from uc_cart.module function uc_cart_view_table().
 
$table['#columns']['qty'] = array(
   
'cell' => t('Qty.'),
   
'weight' => 3,
  );

 
// Example from uc_product.module function uc_product_table_header().
 
$columns['list_price'] = array(
   
'weight' => 3,
   
'access' => FALSE,
   
'cell' => array('data' => t('List price'), 'field' => 'p.list_price'),
  );
?>
The first example is for the quantity field column of the cart view form which is displayed through TAPIr. The ID of the column is qty, it will be sorted using a weight of 3, and its cell data will simply be its title. This would be translated into a header array that looks like:
<?php
  $header
= array(t('Qty.'));
?>
This is pretty straightforward! The second example shows a column that is disabled by default but may be enabled through hook_tapir_table_alter() (more on this later). The cell data is a slightly more complicated usage of a table's header array. In this case, the data will be the text that gets displayed, and the field will be the corresponding table column used by tablesort_sql() to generate an ORDER BY clause for a SELECT query used to load data for this table from the database. This would be translated into a header array that looks like:
<?php
  $header
= array('data' => t('List price'), 'field' => 'p.list_price');
?>
Again, if any column has an access value of FALSE, the whole column will not be displayed when the table is rendered. The rows for the table are generated from the children of the table element. Each row should be an array that includes column specific data and attributes for the row as expected by theme_table(). The cell data array should use the column ID as the key and an array of data the includes a cell key like the header array. Again, I'm sure this is a confusing description, so a simple code example for the quantity column defined in the above example would be:
<?php
 
// Example is modified from uc_cart.module function uc_cart_view_table().
 
$table[] = array(
   
'qty' => array(
     
// Passed in from uc_product_cart_display().
     
'#type' => 'textfield',
     
'#default_value' => $item->qty,
     
'#size' => 5,
     
'#maxlength' => 6,

     
// Added in uc_cart_view_form().
     
'#cell_attributes' => array(
       
'class' => 'qty',
      ),
    ),
   
'#attributes' => array(
     
'valign' => 'top',
    ),
  );
?>
This example defines a row in the table that has data for the qty column. The theme_tapir_table() function adds this element to a table row array so that it looks like this when it passes to theme_table():
<?php
  $row
[] = array(
   
'data' => ..., // This would actually be the result of calling drupal_render() on the 'qty' element.
   
'class' => 'qty',
  );
 
$rows[] = array(
   
'data' => $row,
   
'valign' => 'top',
  );
?>
Sometimes it will be useful to have the actual value of the data represented in the table for use in other queries or operations, as opposed to rendered output. For example, the product price in the cart table as shown above will be rendered for output using the function uc_currency_format(). This adds things like a currency symbol, turning a float value into a string that cannot be used for math operations. Accordingly, the product module adds a theme function to the cell like so:
<?php
 
// Example is modified from uc_cart.module function uc_cart_view_form().
 
$form['items'][$i]['total'] = array(
   
// #type is 'markup' by default.
   
'#value' => $display_item['#total'],
   
'#theme' => 'uc_cart_view_price',
  );
?>
So, putting it all together into a very basic example, the table picture below will display when the table in the following code sample is rendered:
<?php
 
function test_table() {
   
$table = array(
     
'#type' => 'tapir_table',
     
'#title' => t('Test table'),
     
'#attributes' => array('style' => 'width: auto;'),
    );

   
$table['#columns'] = array(
     
'col1' => array(
       
'cell' => array(
         
'data' => t('Column 1'),
         
'style' => 'text-align: center;',
        ),
       
'weight' => 1,
      ),
     
'col2' => array(
       
'cell' => array(
         
'data' => t('Column 2'),
         
'style' => 'text-align: center',
        ),
       
'weight' => 2,
      ),
    );

    for (
$i = 0; $i < 10; $i++) {
     
$table[] = array(
       
'col1' => array(
         
'#value' => $i,
         
'#theme' => 'column_1',
        ),
       
'col2' => array(
         
'#value' => t('Column 2 is bold!'),
         
'#cell_attributes' => array(
           
'style' => 'font-weight: bold;',
          ),
        ),
      );
    }

    return
$table;
  }

  function
theme_column_1($element) {
    return
t('Row @row, column 1.', array('@row' => $element['#value'] + 1));
  }
?>

Rendering tables:

Tables built into forms will be rendered automatically when the form is rendered. If you have a table function that is specifically going to be used for displaying information (i.e. not form elements), you can use the function tapir_get_table() to get the HTML output of the table. So, to render the above example, you could use:
<?php
 
print tapir_get_table('test_table');
?>

Altering and extending tables:

Because TAPIr is modeled after the Forms API, you can alter and extend TAPIr tables using hook_tapir_table_alter() as you would a form. This is useful for reordering the columns in default tables or adding columns to existing tables, like product lists or reports. The following simple example would disable the second column in our test table pictured above, assuming it was placed in a module called tapir_demo.module:
<?php
 
function tapir_demo_tapir_table_alter(&$table, $table_id) {
    if (
$table_id == 'test_table') {
     
$table['#columns']['col2']['access'] = FALSE;
    }
  }
?>
If, however, you want to alter or extend a table that is being used in a form, you should use the familiar hook_form_alter(). All of the data for TAPIr tables are present in the form array, so drupal_get_form() is used in the place of tapir_get_table(). To alter core tables, therefore, you should find out their table/form callback and check against that in your implementation of hook_tapir_table_alter() or hook_form_alter(). Bear in mind that other modules might be altering this table! If you have an alteration that must happen sooner or later than other alterations, you will need to adjust your module's weight in the system table which affects the order hooks functions are called. This is one example of a site specific implementation:
<?php
function example_form_alter(&$form, &$form_state, $form_id) {
 
// Alters the cart view form at /cart.
 
if ($form_id == 'uc_cart_view_form') {
   
// Hides the image column since courses don't have product images.
   
$form['items']['#columns']['image']['access'] = FALSE;

   
// Adds the label to the product description column from the image column.
   
$form['items']['#columns']['desc']['cell'] = t('Products');
  }
}
?>
This function alters the cart view form. Per the instructions above, since this table is part of a form we use hook_form_alter(). The alterations included are commented, namely that the image column will be removed and the product description column will be given a label. This snippet is useful for sites that don't have images on products.