The Form API

by Gisle Hannemyr

It is supplemen­ted by an API for handling forms with user input. Together, they make up a powerful toolset to to capture, display and navigate the content managed by a Drupal website.

Table of contents

Drupal projects discussed in this chapter: Elements.

Introduction

In this chapter, you will learn about the Form API, which is a powerful tool to create forms with fields the user may manipulate.

Form API

The Form API allows the developer to work with HTML forms as a nested associative array of properties and values. When the entire form has been assembled, Drupal's built-in form engine generates the HTML to render the page that contains the form, perform any form validation specified, and generates code to process the form.

This approach offers the following advantages:

The Drupal 7 form engine automatically protects forms against form injection attacks (these are attacks where the user submits form data that is malicious, or has been modified in some malicious way).

Form processing in Drupal has three phases: validation, submission, and redirection.

[More TBA]

A simple form

For our first form, we'll create a simple simple form with a single textfield to ask a user for a “magic word”. If the user types in the wrong word, the form validation will fail. If the word is right, the form will be submitted.

The form will be found on the path simpleform below the siteroot, and only have two elements: a textinput field, and a submit button.

/**
 * Implements hook_menu().
 */
function mymodule_menu() {
  $items['simpleform'] = array(
    'title' => 'A form built with the Form API',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('mymodule_simpleform'),
    'access arguments' => array('access content'), 
    'file' => 'mymodule.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implements the form callback.
 */
function mymodule_simpleform() {
  $form['magic_word'] = array(
    '#title' => t('Please enter the magic word.'),
    '#type' => 'textfield',
    '#description' => t('Hint: The magic word is "cookie"'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit')
  );
  return $form;
}

noteIn hook_menu(), do not run the title through t(). This is to allow the system to cache data, but also display these strings to users using various languages on demand. For more about this, see this page on Drupal.org.

The callback usually resides in a separate file, named as indicated above.

If there is a validation handler and a submit handler, these should be in the same file as the callback.

/**
 * Validatation handler.
 */
function mymodule_simpleform_validate( $form, &$form_state) {
  if ($form_state['values']['magic_word'] != 'cookie') {
    form_set_error('magic_word',
      t('Sorry. You did not type the magic word.'));
  }
}

/**
 * Submission handler.
 */
function mymodule_simpleform_submit($form, &$form_state) {
  $name = $form_state['values']['magic_word'];
  drupal_set_message(t('Thanks for giving me a @name!',
    array('@name' => $name)));
}

Notice the names of the validation handler and submission handler functions. These names are simply the name of the form, followed by the strings “_validate” and “_submit” respectively. These names are the default names for these functions, and don't need to be declared.

The example above shows all the four basic functions you need to handle forms. They are:

  1. A menu hook defining the path to the form.
  2. A function to define the form's two elements as an associative array.
  3. A validation handler.
  4. A submission handler.
The bulk of the work goes into populating the form's data structure, that is, describing the form to Drupal. This information is contained in anested array that describes the elements and properties of the form and is typically contained in a variable called $form.

In the above example, no callback function name for form access control (access callback) is defined in hook_menu. This means that the default function (user_access) is used. It requires one argument, the permission. This is passed as access arguments. In this example, the permission passed is 'access content'.

Add a custom submission handler to a form

array_unshift($form['#submit'], '_rbg_booking_node_form_submit');

Form elements

Below are examples of some of the most common form elements.

Checkboxes, radio buttons and select list

Single checkbox:

$form['mycheckboxp'] = array(
  '#type' =>'checkbox',
  '#title'=>t("Check this box to get a cookie."),
  '#description'=>t("If not checked, no cookie will be awarded."),
  '#default_value' => TRUE,
);

Multiple checkboxes:

$form['mymultiplecb1'] = array(
  '#title' => t('Multiple checkboxes'),
  '#type' => 'checkboxes',
  '#description' => t('Select all that apply'),
  '#default_value' => array(1,2),
  '#options' => array(
    'zero',
    'one',
    'two',
    'three',
  ),
);

You might use an explicit key. It may be a string:

$form['mymultiplecb'] = array(
  '#title' => t('Multiple checkboxes'),
  '#type' => 'checkboxes',
  '#description' => t('Select all that apply'),
  '#default_value' => array('onek','twok'),
  '#options' => array(
    'zerok' => 'zero',
    'onek' => 'one',
    'twok' => 'two',
    'threek' => 'three',
  ),
);

Using an explicit key also works for radio buttons and select lists.

Radio buttons:

$form['myradio'] = array(
  '#type' => 'radios',
  '#title' => t('Select one item'),
  '#description' => t('You should just pick one.'),
  '#default_value' => variable_get('myradio', 2),
  '#options' => array(
    'zero',
    'one',
    'two',
    'three',
  ),
);

Select list:

$form['myselect'] = array(
  '#type' => 'select',
  '#title' => t('Select one item'),
  '#description' => t('You should just pick one.'),
  '#default_value' => variable_get('myselect', 2),
  '#options' => array(
     'zero',
     'one',
     'two',
     'three',
  ),
);

Date

https://www.drupal.org/forum/support/post-installation/2006-01-27/form-items-inline
$form['mydate'] = array(
  '#type' => 'date_text',
  '#date_format' => 'Y-m-d',
  '#default_value' => 0,
$form['mydate'] = array(
  '#type' => 'date_select',
  '#title' => t('Date'),
  '#date_format' => 'Y-m-d',
  '#date_year_range' => '-3:+3',
  '#default_value' => date('Y-m-d'),
  '#default_value' => 0,
  '#description' => t('Use the pull-down menus.'),
  '#required' => TRUE,
);
'#type' => 'date_popup', '#date_year_range' => '-3:+3', '#default_value' => date('Y-m-d'),
$form['date'] = array(
  '#type' => 'date_text',
  '#bdtpicker' => TRUE,
  '#date_format' => 'Y-m',
  '#default_value' => $current,
  '#datepicker_options' => array(
    'viewMode' => 'months',
  ),
  '#ajax' => array(
    'callback' => 'rbg_date_picker_callback',
  ),
);

File

The form element to upload a file may look like this:

$form['myfile'] = array(
  '#type' => 'file',
  '#title' => t('Image'),
  '#description' => t('Upload a file, allowed extensions: jpg, jpeg, png, gif'),
);

Drupal splits files in two categories: managed or unmanaged. The difference between the two lies in the way the files are used.

Managed files work hand in hand with the Entity system and are in fact tied to File entities. So whenever we create a managed file, an entity gets created for it as well, which we can use in all sorts of ways. A record of these file entities are stored in a table called {file_managed}. A key aspect of managed files is the fact that their usage is tracked. This means that if we attach a manged file to another entity, reference it, or even manually indicate that we use it, this usage is tracked in a secondary table called {file_usage}. This way, we can see where each file is used and how many times, and Drupal even provides a way to delete “orphaned” files automatically.

Unmanaged files are not tracked and are not entities. They just live in the file system and can be linked if we know their path.

Processing an uploaded managed or unmanaged file requires custom validation and submit handlers.

See alsoTo learn how to upload a managed file, see Form example tutorial 10 @ Drupal.org. For an example of how to process managed files without using the Form API, see this tutorial by Patrick J Waters.

The example below shows the scaffolding required to have a form element that let users upload an unmanged file.

First, define the form containing a form element of the type “file” and a submit button.:

/**
 * Implements hook_form
 */
function mymodule_form($form, &$form_state) {
  $form['img_file'] = array(
    '#type' => 'file',
    '#title' => t('Select the image to upload'),
    '#description' => t('Upload a file, allowed extensions: jpg jpeg png gif'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit')
  );
  return $form;
}

Provide a custom form submit function:

/**
 * Custom form submit function.
 */
function mymodule_form_submit($form, &$form_state) {
  // We are done with the file, remove it from storage.
  unset($form_state['storage']['file']);
  // Set a response to user to provide closure.
  drupal_set_message(t('The image has been saved.'));
}

Most of the heavy lifting is done by the validate function by means of file_save_upload(). Since the uploaded file must be an image, we use the validation function file_validate_is_image() to verify that the file is really an image. We also use file_validate_extensions() to add .txt to uploads with other extensions.

/**
 * Custom form validate function.
 */
function mymodule_form_validate($form, &$form_state) {
  $validators = array(
    'file_validate_is_image' => array(),
    'file_validate_extensions' => array('jpg jpeg png gif'),
  );
  $file = file_save_upload('img_file', $validators, FALSE, FILE_EXISTS_REPLACE);
  // If the file passed validation:
  if ($file) {
    // Move the file into the Drupal public file system:
    $publicfile = file_unmanaged_move($file->uri, 'public://', FILE_EXISTS_REPLACE);
    if (!$publicfile) {
      form_set_error('file', t('Unable to move uploaded file to the destination.'));
    }
  }
  else {
    form_set_error('file', t('No file was uploaded.'));
  }
}

Grouping

The Drupal 7 Form API provides two types for grouping elements: container and fieldset. In Drupal 8, there is also detail.

The container type returns HTML to wrap child elements in a container. It surrounds child elements with a <div> and adds attributes such as classes or an HTML id.

$form['mycontainer] = array(
  '#type' => 'container', 
  '#attributes' => array('class' => array('container-inline')),
  '#access' => TRUE,
);

The fieldset type wraps the set in a frame and let you organize form elements which need to be grouped logically inside the frame.

$form['mycontainer'] = array(
  '#type' => 'fieldset', 
  '#title' => t('Title'), 
  '#attributes' => array('class' => array('container-inline')),
  '#access' => TRUE,
);

By default, members of a fieldset is displayed on separate, which makes for a very long page. this answer on Drupal SE explains how to use CSS to display field inputs side-by-side.

See also: Drupal.org: Fieldsets.

Markup

$form['mymarkup'] = array(
  '#markup' => '<p>Some markup.</p>',
);

Numbers

For input of integer and decimal numbers, use a single line textfield.

$form['myinteger'] = array(
  '#type' => 'textfield',
  '#title' => t('My integer'),
  '#default_value' => variable_get('myint', 0),
  '#size' => 3,
  '#maxlength' => 5,
  '#description' => t('You should type an integer.'),
);

To be able to use the HTML5 type number (rendered with stepper arrows by most browsers), you need to enable the Elements module.

$form['myquantity'] = array(
  '#type'          => 'numberfield',
  '#title'         => t('Quantity'),
  '#default_value' => 1,
  '#step'          => 2,
  '#max'           => 9,
  '#min'           => 0,
);

Text

A single line textfield:

$form['myform'] = array(
  '#title' => t('Please enter the magic word.'),
  '#type' => 'textfield',
  '#description' => t('Hint: The magic word is "cookie"'),
);

A multiline textarea:

$form['mytextarea'] = array(
  '#title' => t('A text area'),
  '#type' => 'textarea',
  '#description' => 'Please write your story in the form.',
  '#default_value' => 'This is my story ...',
  '#rows' => 5,
  '#resizable' => TRUE,
);

Final word

[TBA]


Last update: 2019-06-21 [gh].