Theming Drupal 7 Form Elements in a Table

The introduction of render arrays in Drupal 7 is a useful addition over D6. It allows you to build out HTML using an array notation, very similar to how FAPI has worked in the past. This allows you to take control of your output, and for this article, specifically theming.

One common task is to build a form where the elements are displayed in a table - and there seems to be a lack of good documentation for doing this.

You would think it would be as simple as a normal renderable array where you could do something like the following (this doesn't work):
View Gist on GitHub

<?php

function formtable_form_table_form($form = array(), &$form_state) {
  $form['table'] = array(
    '#theme' => 'table',
    '#header' => array(t('Column 1'), t('Column 2')),
    '#rows' => array(
      // First row.
      'r1' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 2'),
        ),
      ),
      // Second row.
      'r2' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 2'),
        ),
      ),
    ),
  );

  // Add a submit button for fun.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
}

In FAPI (and renderable arrays in general), keys starting with a # are considered as properties, not renderable elements - meaning their children aren't processed for content. Therefore, getting form elements into a table involves creating a custom theme callback and rendering the table within the callback.

Building the form

Building the form is similar to any other, with a couple differences noted below:
View Gist on GitHub

<?php
/**
 * Page Callback / Form Builder for the table form.
 */
function formtable_form_table_form($form = array(), &$form_state) {
  $form['table'] = array(
    // Theme this part of the form as a table.
    '#theme' => 'formtable_form_table',
    // Pass header information to the theme function.
    '#header' => array(t('Column 1'), t('Column 2')),
    // Rows in the form table.
    'rows' => array(
      // Make it a tree for easier traversing of the entered values on submission.
      '#tree' => TRUE,
      // First row.
      'r1' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 2'),
        ),
      ),
      // Second row.
      'r2' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 2'),
        ),
      ),
    ),
  );

  // Add a submit button for fun.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  return $form;
}

Register the theme callback

Creating the theme callback is also straightforward. You would create it as any other, but change the render element as below:
View Gist on GitHub

<?php
/**
 * Implementation of hook_theme().
 */
function formtable_theme() {
  return array(
    'formtable_form_table' => array(
      // The renderable element is the form.
      'render element' => 'form',
    ),
  );
}

Create the theme callback

View Gist on GitHub

<?php

/**
 * Theme callback for the form table.
 */
function theme_formtable_form_table(&$variables) {
  // Get the useful values.
  $form = $variables['form'];
  $rows = $form['rows'];
  $header = $form['#header'];

  // Setup the structure to be rendered and returned.
  $content = array(
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => array(),
  );

  // Traverse each row.  @see element_chidren().
  foreach (element_children($rows) as $row_index) {
    $row = array();
    // Traverse each column in the row.  @see element_children().
    foreach (element_children($rows[$row_index]) as $col_index) {
      // Render the column form element.
      $row[] = drupal_render($rows[$row_index][$col_index]);
    }
    // Add the row to the table.
    $content['#rows'][] = $row;
  }

  // Redner the table and return.
  return drupal_render($content);
}

The full module file

View Gist on GitHub

<?php

/**
 * Implementation of hook_menu().
 */
function formtable_menu() {
  $items = array();

  // A page to demonstrate theming form elements in a table.
  $items['formtable'] = array(
    'title' => 'Form Table Example',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('formtable_form_table_form'),
    'access callback' => TRUE,
  );

  return $items;
}

/**
 * Implementation of hook_theme.
 */
function formtable_theme() {
  return array(
    'formtable_form_table' => array(
      // The renderable element is the form.
      'render element' => 'form',
    ),
  );
}

/**
 * Page Callback / Form Builder for the table form.
 */
function formtable_form_table_form($form = array(), &$form_state) {
  $form['table'] = array(
    // Theme this part of the form as a table.
    '#theme' => 'formtable_form_table',
    // Pass header information to the theme function.
    '#header' => array(t('Column 1'), t('Column 2')),
    // Rows in the form table.
    'rows' => array(
      // Make it a tree for easier traversing of the entered values on submission.
      '#tree' => TRUE,
      // First row.
      'r1' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 1 Column 2'),
        ),
      ),
      // Second row.
      'r2' => array(
        'c1' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 1')
        ),
        'c2' => array(
          '#type' => 'textfield',
          '#title' => t('Row 2 Column 2'),
        ),
      ),
    ),
  );

  // Add a submit button for fun.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  return $form;
}

/**
 * Theme callback for the form table.
 */
function theme_formtable_form_table(&$variables) {
  // Get the userful values.
  $form = $variables['form'];
  $rows = $form['rows'];
  $header = $form['#header'];

  // Setup the structure to be rendered and returned.
  $content = array(
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => array(),
  );

  // Traverse each row.  @see element_chidren().
  foreach (element_children($rows) as $row_index) {
    $row = array();
    // Traverse each column in the row.  @see element_children().
    foreach (element_children($rows[$row_index]) as $col_index) {
      // Render the column form element.
      $row[] = drupal_render($rows[$row_index][$col_index]);
    }
    // Add the row to the table.
    $content['#rows'][] = $row;
  }

  // Redner the table and return.
  return drupal_render($content);
}

Comments

Luis José's picture

Luis José

Great article! Really well explained, thank you! In "The full module file", the line "return $items" is missing in hook_menu.

Sam's picture

Sam

Thanks for the note! Updated.

chase's picture

chase

This was a big help! One thing I noticed, perhaps specific to my Drupal, was for each column array, I had to format as....

  $form['table'] = array(
  'rows' => array(
    'r1' => array(
      'c1' => array(
        'data' => array('#title' => t('bla')),
      ),
    ),
  );

...but other than that, this was striking gold for me! Thank you!

Sam's picture

Sam

That's correct. When you specify a string for a column, it is not processed and displayed directly. However, if the column is an array, you can insert a renderable element into the value for 'data' key. There's some more info in this comment.

Himanshu's picture

Himanshu

How to render Pager in this form ? Where to use theme('pager') so that rows of my form become limited on a single page.

Sam's picture

Sam

Though this example doesn't use a database query, you would need one for paging.

To get a pager, first add $query->extend('PagerDefault')->limit(20) in your db_select() when querying to get the $rows variable.

Next add $form['pager'] = ['#theme' => 'pager']; somewhere in your form builder, probably above or below the submit button.

More info on PagerDefault, theme_pager, and how the pager works.

himanshu's picture

himanshu

Thank you for your quick response :)

Tom's picture

Tom

This literally saved me from a huge re-write when I realized I'd created a static form themed as a Drupal table. Thanks so much for your detailed post! I will use and re-use this example.

ANDREz's picture

ANDREz

Awesome article buddy, so easy to follow

smita's picture

smita

Nice post.. It is really helpful...the full module file is working perfect .

Pages