Skip to content
form_example_elements.inc 13.6 KiB
Newer Older
<?php

// $Id$

/**
 * @file
 * This is an example demonstrating how a module can define custom form
 * elements.
 *
 * Form elements are already familiar to anyone who uses Form API. Examples
 * of core form elements are 'textfield', 'checkbox' and 'fieldset'. Drupal
 * utilizes hook_elements() to define these FAPI types, and this occurs in
 * the core function system_elements().
 *
 * Each form element has a #type value that determines how it is treated by
 * the Form API and how it is ultimately rendered into HTML. hook_elements()
 * allows modules to define new element types, and tell the Form API what
 * default values they should automatically be populated with.
 *
 * By implementing hook_elements in your own module, you can create custom
 * form elements with their own properties, validation and theming.
 *
 * In this example, we define a series of elements that range from trivial
 * (a renamed textfield) to more advanced (a telephone number field with each
 * portion separately validated).
 *
 * The @link http://drupal.org/node/169815 Elements handbook page @endlink
 * has full details on creating elements. See also hook_elements().
 */


/**
 * Implementation of hook_elements().
 *
 * This defines a new form element types.
 *
 * - form_example_textfield: This is actually just a textfield, but provides
 *   the new type. If more were to be done with it a theme function could be
 *   provided.
 * - form_example_checkbox: Nothing more than a regular checkbox, but uses
 *   an alternate theme function provided by this module.
 * - form_example_phonenumber_discrete: Provides a North-American style
 *   three-part phonenumber where the value of the phonenumber is managed
 *   as an array of three parts.
 * - form_example_phonenumber_combined: Provides a North-American style
 *   three-part phonenumber where the actual value is managed as a 10-digit
 *   string and only broken up into three parts for the user interface.
 *
 * See hook_elements() and the
 * @link http://drupal.org/node/169815 Creating Custom Elements @endlink
 * handbook page.
 */
function _form_example_elements() {
  // Simple elements based on textfield require only a definition and a theme
  // function. In this case we provide the theme function using the default
  // but it would by default be provided in hook_theme(), probably as
  // theme_form_example_textfield().
  $types['form_example_textfield'] = array(
    // #input tells FAPI that this is an element that will carry a value, even
    // if it is a hidden value.
    '#input' => TRUE,
    '#theme' => array('textfield'),
    '#autocomplete_path' => FALSE,
  );

  $types['form_example_checkbox'] = array(
    '#input' => TRUE,
    '#return_value' => TRUE,
    '#process' => array('form_expand_ahah'),
    // #theme is the default theme('form_example_checkbox'), provided by
    // this module.

    // This also depends on the existence of
    // form_type_form_example_checkbox_value(), which is provided by this
    // module. Since this is not a default textfield-derived element, it
    // needs its own value callback.
  );

	// This discrete phonenumber element keeps its values as the separate elements
	// area code, prefix, extension.
  $types['form_example_phonenumber_discrete'] = array(
    '#input' => TRUE,

    // #process is an array of callback functions executed when this element is
    // processed. Here it provides the child form elements which define
    // areacode, prefix, and extension.
    '#process' => array('form_example_phonenumber_discrete_process'),

    // validation handlers for this element
    '#element_validate' => array('form_example_phonenumber_discrete_validate'),
    '#autocomplete_path' => FALSE,
  );

  // Define form_example_phonenumber_combined, which combines the phone
  // number into a single validated text string.
  $types['form_example_phonenumber_combined'] = array(
    '#input' => TRUE,
    '#process' => array('form_example_phonenumber_combined_process'),
    '#element_validate' => array('form_example_phonenumber_combined_validate'),
    '#autocomplete_path' => FALSE,
    '#default_value' => array(
      'areacode' => '',
      'prefix' => '',
      'extension' => '',
    ),
  );
  return $types;
}

/**
 * Helper function to determine the value for an form_example_checkbox.
 *
 * Required for the element type 'form_example_checkbox' to work.
 * Copied from form.inc.
 *
 * @param $form
 *   The form element whose value is being populated.
 * @param $edit
 *   The incoming POST data to populate the form element. If this is FALSE,
 *   the element's default value should be returned.
 * @return
 *   The data that will appear in the $form_state['values'] collection
 *   for this element. Return nothing to use the default.
 */
function form_type_form_example_checkbox_value($form, $edit = FALSE) {
  if ($edit !== FALSE) {
    if (empty($form['#disabled'])) {
      return !empty($edit) ? $form['#return_value'] : 0;
    }
    else {
      return $form['#default_value'];
    }
  }
}

/**
 * Process callback for the discrete version of phonenumber.
 */
function form_example_phonenumber_discrete_process($element, $edit, &$form_state, $complete_form) {
	// #tree = TRUE means that the values in $form_state['values'] will be stored
	// hierarchically. In this case, the parts of the element will appear in
	// $form_state['values'] as
	// $form_state['values']['<element_name>']['areacode'],
	// $form_state['values']['<element_name>']['prefix'],
  // etc. This technique is preferred when an element has member form
  // elements.
  $element['#tree'] = TRUE;

  // Normal FAPI field definitions, except that #value is defined.
  $element['areacode'] = array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#value' => $element['#value']['areacode'],
    '#required' => TRUE,
    '#prefix' => '(',
    '#suffix' => ')',
  );
  $element['prefix'] =  array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#required' => TRUE,
    '#value' => $element['#value']['prefix'],
  );
  $element['extension'] =  array(
    '#type' => 'textfield',
    '#size' => 4,
    '#maxlength' => 4,
    '#value' => $element['#value']['extension'],
  );

  return $element;
}

/**
 * Validation handler for the discrete version of the phone number.
 *
 * Using regular expressions, we check that:
 *  - the area code is a three digit number
 *  - the prefix is numeric 3-digit number
 *	- the extension is a numeric 4-digit number
 *
 * Any problems are shown on the form element using form_error().
 */
function form_example_phonenumber_discrete_validate($element, &$form_state) {
  if (isset($element['#value']['areacode'])) {
    if (0 == preg_match('/^\d{3}$/', $element['#value']['areacode'])) {
      form_error($element['areacode'], t('The area code is invalid.'));
    }
  }
  if (isset($element['#value']['prefix'])) {
    if (0 == preg_match('/^\d{3}$/', $element['#value']['prefix'])) {
      form_error($element['prefix'], t('The prefix is invalid.'));
    }
  }
  if (isset($element['#value']['extension'])) {
    if (0 == preg_match('/^\d{4}$/', $element['#value']['extension'])) {
      form_error($element['extension'], t('The extension is invalid.'));
    }
  }
  return $element;
}



/**
 * Process callback for the combined version of the phonenumber element.
 */
function form_example_phonenumber_combined_process($element, $edit, &$form_state, $complete_form) {
  // #tree = TRUE means that the values in $form_state['values'] will be stored
  // hierarchically. In this case, the parts of the element will appear in
  // $form_state['values'] as
  // $form_state['values']['<element_name>']['areacode'],
  // $form_state['values']['<element_name>']['prefix'],
  // etc. This technique is preferred when an element has member form
  // elements.
  $element['#tree'] = TRUE;


  // Normal FAPI field definitions, except that #value is defined.
  $element['areacode'] = array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#required' => TRUE,
    '#prefix' => '(',
    '#suffix' => ')',
  );
  $element['prefix'] =  array(
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#required' => TRUE,
  );
  $element['extension'] =  array(
    '#type' => 'textfield',
    '#size' => 4,
    '#maxlength' => 4,
    '#required' => TRUE,
  );


  $matches = array();
  $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
  if ($match) {
    array_shift($matches); // get rid of the "all match" element
    list($element['areacode']['#default_value'], $element['prefix']['#default_value'], $element['extension']['#default_value']) = $matches;
  }
  return $element;
}

/**
 * Combined version validation function.
 *
 * Using regular expressions, we check that:
 *  - the area code is a three digit number
 *  - the prefix is numeric 3-digit number
 *  - the extension is a numeric 4-digit number
 *
 * Any problems are shown on the form element using form_error().
 *
 * The combined value is then updated in the element.
 */
function form_example_phonenumber_combined_validate($element, &$form_state) {
  $lengths = array(
    'areacode' => 3,
    'prefix' => 3,
    'extension' => 4,
  );
  foreach ($lengths as $member => $length) {
    $regex = '/^\d{' . $length . '}$/';
    if (!empty($element['#value'][$member]) && 0 == preg_match($regex, $element['#value'][$member])) {
      form_error($element[$member], t('@member is invalid', array('@member' => $member)));
    }
  }

  // Consolidate into the three parts into one combined value.
  $value = $element['#value']['areacode'] . $element['#value']['prefix'] . $element['#value']['extension'];
  form_set_value($element, $value, $form_state);
  return $element;
}

/**
 * Called by form_example_theme() to provide hook_theme().
 */
function _form_example_element_theme() {
  return array(
    'form_example_checkbox' => array(
      'arguments' => array('element'),
      'file' => 'form_example_elements.inc',
    ),
    'form_example_phonenumber_discrete' => array(
      'arguments' => array('element'),
      'file' => 'form_example_elements.inc',
    ),
    'form_example_phonenumber_combined' => array(
      'arguments' => array('element'),
      'file' => 'form_example_elements.inc',
    ),
  );
}


/**
 * Theme function for form_example_checkbox type.
 */
function theme_form_example_checkbox($element) {
  return theme('checkbox', $element);
}

/**
 * Theme function to format the discrete version.
 *
 * We use the container-inline class so that all three of the HTML elements
 * are placed next to each other, rather than on separate lines.
 */
function theme_form_example_phonenumber_discrete($element) {
	// #children represents all the sublevels elements already rendered in HTML.
	// Here it contains the three parts of the 'phonenumber' element type ('areacode', 'prefix' and 'extension').
  return theme('form_element', $element, '<div class="container-inline">' . $element['#children'] . '</div>');
}


/**
 * Theme function to format the combined version.
 *
 * We use the container-inline class so that all three of the HTML elements
 * are placed next to each other, rather than on separate lines.
 */
function theme_form_example_phonenumber_combined($element) {
  // #children represents all the sublevels elements already rendered in HTML.
  // Here it contains the three parts of the 'phonenumber' element type ('areacode', 'prefix' and 'extension').
  return theme('form_element', $element, '<div class="container-inline">' . $element['#children'] . '</div>');
}

/**
 * This is a simple form to demonstrate how to use the various new FAPI elements
 * we've defined.
 */
function form_example_element_demo_form() {
  $form['form_example_textfield'] = array(
    '#type' => 'form_example_textfield',
    '#title' => t('Form Example textfield'),
    '#default_value' => variable_get('form_example_textfield', ''),
    '#description' => t('form_example_textfield is a new type, but it is actually uses the system-provided functions of textfield'),
  );

  $form['form_example_checkbox'] = array(
    '#type' => 'form_example_checkbox',
    '#title' => t('Form Example checkbox'),
    '#default_value' => variable_get('form_example_checkbox', FALSE),
    '#description' => t('Nothing more than a regular checkbox but with a theme provided by this module.')
  );

  $form['form_example_element_discrete'] = array(
    '#type' => 'form_example_phonenumber_discrete',
    '#title' => t('Discrete phone number'),
    '#default_value' => variable_get('form_example_element_discrete', array('areacode' => '', 'prefix' => '', 'extension' => '')),
    '#description' => t('A phone number : areacode (XXX), prefix (XXX) and extension (XXXX). This one uses a "discrete" element type, one which stores the three parts of the telephone number separately.'),
  );

  $form['form_example_element_combined'] = array(
    '#type' => 'form_example_phonenumber_combined',
    '#title' => t('Combined phone number'),
    '#default_value' => variable_get('form_example_element_combined', '0000000000'),
    '#description' => t('form_example_element_combined one uses a "combined" element type, one with a single 10-digit value which is broken apart when needed.'),
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  return $form;
}

/**
 * Submit handler for form_example_element_demo_form().
 */
function form_example_element_demo_form_submit($form, &$form_state) {
  // Exclude unnecessary elements.
  unset($form_state['values']['submit'], $form_state['values']['form_id'], $form_state['values']['op'], $form_state['values']['form_token'], $form_state['values']['form_build_id']);

  foreach ($form_state['values'] as $key => $value) {
    variable_set($key, $value);
    drupal_set_message(t('%name has value %value', array('%name' => $key, '%value' => print_r($value, TRUE))));
  }
}