Theming form elements

by Gisle Hannemyr

This chapter explains how to do customize the display of form elements.

Table of contents

Introduction

The majority of the work theming a Drupal 7 form can be done using the default output of the form from Drupal 7. In some cases, you may want to tweak some CSS for the layout and form element sizes.

Sometimes, however, you need better control over the HTML output of your form.

See alsoSee also: Drupal SE: How do I theme a form element? and H. Bensalem: Theming the login form.

Use #prefix and #suffix wrappers

You may use #prefix and #suffix wrappers in your form defintion to provide additional classes:

form['submit'] = array(
  '#type' => 'submit',
  '#value' => t('Submit'),
  '#attributes' => array('style' => 'padding: 0 10px;'),
  '#theme' => "submit",
  '#prefix' => '<div class="submit-button-custom">',
  '#suffix' => '</div>',
);

Alter the form

You may provide a form-specific alteration (instead of the global hook_form_alter()).

/**
 * Implements hook_form_FORM_ID_alter().
 */
function YOUR_THEME_form_FORM_ID_alter(&$form, &$form_state, $form_id) {
  $form['submit']['#prefix'] = '<div class="submit-button-custom">';
  $form['submit']['#sufix'] = '</div>';
}

Theming form elements as a table

For example, how can an input form use a table to lay out the fields? This section will describe how to do this with a simple 2×2 layout.

You can download the code of this project from Drupal.org:

$ git clone --branch 7.x-1.x http://git.drupal.org/sandbox/gisleh/2226115.git inf5272
cd inf5272 

The code is in the subdirectory formtable.

Create the .info-file

Since this is going to be a demonstration module, there should be a .info-file. Put the following into formtable.info.

name = Formtable
description = A demonstration of how to theme form elements as a table.
core = 7.x

Create the schema for the data

Before we can start building the module, we need to create a schema to hold the data. We are going to provide for a heading, and a 2×2 layout. The following should be in formtable.install:

/**
 * Implements hook_schema().
 */
function formtable_schema() {
  $schema['formtable'] = array(
    'fields' => array(
      'ftid' => array(
        'description' => 'primary key',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'title' => array(
        'description' => 'The title field',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'r1c1' => array(
        'description' => 'Row 1 Column 1',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'r1c2' => array(
        'description' => 'Row 1 Column 2',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'r2c1' => array(
        'description' => 'Row 2 Column 1',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
      'r2c2' => array(
        'description' => 'Row 2 Column 2',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
      ),
    ),
    'primary key' => array('ftid'),
  );
  return $schema;
}

Build the form

Drupal 7 makes extensive use of render arrays to separate the content of a form from its presentation. In addition to the renderable elements in a form, you may add properties (starting with the character #). In the form below, these are used to identify the theme function to use, the items that go into the table headers, and the type and title of the fields.

/**
 * Form builder for the formtable demo form.
 */
function formtable_form($form = array(), &$form_state) {

  $form['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Title'),
      '#required' => TRUE,
      '#description' => t('This field is not part of the table.'),
  );

  $form['table'] = array(
    // The next line specify form what theme function to use.
    '#theme' => 'formtable_tablepart',
    // Pass the 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 easy traversing of the 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 the submit button.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  return $form;
}

Add validation and submit handlers

As always, we need to add a valdation handler and a submit handler to process form data. To be discovered they must be named with the name of the module (formtable) followed by the name of the form (form) followed by (validate) and (submit) respectively.

This is the form validation handler. It just makes sure that no field is empty.

/**
 * Form validatation handler.
 */
function formtable_form_validate( $form, &$form_state) {
  if (empty($form_state['values']['rows']['r1']['c1'])) {
    form_set_error('rows][r1][c1', t('Field cannot be empty.'));
  }
  if (empty($form_state['values']['rows']['r1']['c2'])) {
    form_set_error('rows][r1][c2', t('Field cannot be empty.'));
  }
  if (empty($form_state['values']['rows']['r2']['c1'])) {
    form_set_error('rows][r2][c1', t('Field cannot be empty.'));
  }
  if (empty($form_state['values']['rows']['r2']['c2'])) {
    form_set_error('rows][r2][c2', t('Field cannot be empty.'));
  }
}

The form submission handler just inserts the content of the form into the table formtable in the database, and prints a message on the screen.

/**
 * Form submission handler.
 */
function formtable_form_submit($form, &$form_state) {
  $key = db_insert('formtable')
  ->fields(array(
    'title' => $form_state['values']['title'],
    'r1c1' => $form_state['values']['rows']['r1']['c1'],
    'r1c2' => $form_state['values']['rows']['r1']['c2'],
    'r2c1' => $form_state['values']['rows']['r2']['c1'],
    'r2c2' => $form_state['values']['rows']['r2']['c2'],
  ))
  ->execute();
  drupal_set_message(t('Form submitted.'));
  drupal_goto('formtable/' . $key);
}

Create and register the theme callback

The theme callback is a functions that actually themes the table part of the form. Its name must start with theme_. The rest of the name (here formtable_tablepart) is used in hook_theme to identify this function.

We're going to use the built-in theme_table theme function to render the themed table. This function is used to build that render array.

From the documentation of theme_table, we see that this function accepts an associative array where the table header and rows are both arrays. Both simple arrays with just the text to show, and more complex arrays listing attributes are allowed. This example shows how these arrays may look like:

$header = array('Header 1 ', 'Header 2', 'Header 3');
$rows = array(
  // Simple row
  array(
    'Cell 1', 'Cell 2', 'Cell 3',
  ),
  // Row with attributes on Cell 4 and the row.
  array(
    'data' => array(
      array('data' => 'Cell 4', 'class' => 'formtable-td', 'colspan' => 2),
      'Cell 5',
    ),
    'class' => array('formtable-tr'),
  )
);

In this demonstration module, we are just going to construct simple arrays for both arrays, based upon the the form defined in the function formtable_form above:

/**
 * Theme callback to theme part of the booking form in a table format.
 */
function theme_formtable_tablepart($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;
  }

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

Note that to be discoverable, the name of this function need to be registered with hook_theme as the theme for the form. Otherwise, it will be ignored:

/**
 * Implements hook_theme($existing, $type, $theme, $path).
 */
function formtable_theme() {
  return array(
    'formtable_tablepart' => array(
      'render element' => 'form',
    ),
  );
}

To make this a working module that can be tested, we need to add a menu router. The menu router provides access to the path with the form, as well as a router to a function that displays the form data.

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

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

  $items['formtable/%'] = array(
    'title' => 'FormTable item',
    'page callback' => 'formtable_showitem',
    'page arguments' => array(1),
    'access callback' => TRUE,
  );

  return $items;
}

function formtable_showitem($ftid) {
  $item = db_query("SELECT * from {formtable}
    WHERE ftid = :ftid
    LIMIT 1", array(":ftid" => $ftid))->fetchObject();
  dpm($item, 'item');
  $content = '<h3>' .  $item->title . '</h3>';
  $content .= '<table><tr><td>' . $item->r1c1 . '</td>';
  $content .= '<td>' . $item->r1c2 . '</td></tr>';
  $content .= '<tr><td>' . $item->r2c1 . '</td>';
  $content .= '<td>' . $item->r2c2 . '</td></tr></table';
  return $content;
}

Add hook_help

Having a hook_help in the module is not strictly necessary. However, the implementation of hook_help below makes it simple to test the module by providing a clickable link to the form's path.

/**
 * Implements hook_help().
 */
function formtable_help($path, $arg) {
  switch ($path) {
    case 'admin/help#formtable':
      $output = t('<p>A demonstrator that shows how theme form elements
        as a table. To test, navigate to the path !path.</p>',
	array('!path' => l('formtable','formtable')));
      return $output;
  }
}

Note that we can also test the module by typing this path by hand in the web browser's address field.

Deploy and test

After installing and enabling the module, to see the module in action, navigate to the path formtable just below the siteroot. As noted in the previous section, there is also a clickable link to this path on the module's help page.

Final word

You may also create a multiple field widget that allows widgets containing multiple fields to be embedded in any Drupal entity. An example of this can be found in the phase 2 blog. Unfortunately, this example is rather buggy, so use it as inspiration, not as a recipe.


Last update: 2015-08-24 [gh].