Index: form_example.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/examples/form_example/form_example.info,v
retrieving revision 1.4
diff -u -r1.4 form_example.info
--- form_example/form_example.info	11 Aug 2010 23:16:27 -0000	1.4
+++ form_example/form_example.info	2 Oct 2010 22:46:17 -0000
@@ -8,4 +8,5 @@
 files[] = form_example.install
 files[] = form_example_tutorial.inc
 files[] = form_example_states.inc
+files[] = form_example_elements.inc
 files[] = form_example.test
Index: form_example.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/examples/form_example/form_example.module,v
retrieving revision 1.7
diff -u -r1.7 form_example.module
--- form_example/form_example.module	20 Jun 2010 19:21:48 -0000	1.7
+++ form_example/form_example.module	3 Oct 2010 00:23:45 -0000
@@ -127,11 +127,20 @@
     'description' => 'How to use the #states attribute in FAPI',
     'file' => 'form_example_states.inc',
   );
+  $items['examples/form_example/element_example'] = array(
+    'title' => 'Element example',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_example_element_demo_form'),
+    'access callback' => TRUE,
+    'file' => 'form_example_elements.inc',
+    'weight' => 100,
+  );
+
   return $items;
 }
 
 function form_example_intro() {
-  $markup = t('The form example module provides a tutorial and a #states example');
+  $markup = t('The form example module provides a tutorial, an element example, and a #states example');
   return $markup;
 }
 
@@ -139,12 +148,38 @@
  * Implements hook_help() to provide a bit of help.
  */
 function form_example_help($path, $arg) {
-  switch($path) {
+  switch ($path) {
     case 'examples/form_example/tutorial':
       // TODO: Update the URL.
       $help = t('This form example tutorial for Drupal 7 is the code from the <a href="http://drupal.org/node/262422">Handbook 10-step tutorial</a>');
+      break;
+    case 'examples/form_example/element_example':
+      $help = t('The Element Example shows how modules can provide their own Form API element types. Four different element types are demonstrated.');
+      break;
   }
   if (!empty($help)) {
     return '<p>' . $help . '</p>';
   }
-}
\ No newline at end of file
+}
+
+/**
+* Implementation of form_example_elements().
+*
+* To keep the various pieces of the example together, this just returns
+* _form_example_elements().
+*/
+function form_example_element_info() {
+  require_once('form_example_elements.inc');
+  return _form_example_element_info();
+}
+
+/**
+* Implementation of hook_theme().
+*
+* To keep the various parts of the example together, this actually returns
+* _form_example_element_theme().
+*/
+function form_example_theme($existing, $type, $theme, $path) {
+  require_once('form_example_elements.inc');
+  return _form_example_element_theme($existing, $type, $theme, $path);
+}
Index: form_example.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/examples/form_example/form_example.test,v
retrieving revision 1.8
diff -u -r1.8 form_example.test
--- form_example/form_example.test	1 Sep 2010 11:44:40 -0000	1.8
+++ form_example/form_example.test	3 Oct 2010 00:26:06 -0000
@@ -13,8 +13,8 @@
 
   public static function getInfo() {
     return array(
-      'name' => 'Form Example tests',
-      'description' => 'Various tests on the dbtng example module.' ,
+      'name' => 'Form Example',
+      'description' => 'Various tests on the form_example module.' ,
       'group' => 'Examples',
     );
   }
@@ -106,5 +106,35 @@
       $this->assertText(t('@num: firstname @num lastname @num (@year)', array('@num' => $i, '@year' => 1950 + $i)));
     }
   }
-}
 
+ /**
+  * Test the element_example form for correct behavior.
+  */
+ function testElementExample() {
+   // Make one basic POST with a set of values and check for correct responses.
+   $edit = array(
+     'form_example_textfield' => $this->randomName(),
+     'form_example_checkbox' => TRUE,
+     'form_example_element_discrete[areacode]' => sprintf('%03d', rand(0,999)),
+     'form_example_element_discrete[prefix]' => sprintf('%03d', rand(0,999)),
+     'form_example_element_discrete[extension]' => sprintf('%04d', rand(0,9999)),
+     'form_example_element_combined[areacode]' => sprintf('%03d', rand(0,999)),
+     'form_example_element_combined[prefix]' => sprintf('%03d', rand(0,999)),
+     'form_example_element_combined[extension]' => sprintf('%04d', rand(0,9999)),
+   );
+   $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+   $this->assertText(t('form_example_textfield has value @value', array('@value' => $edit['form_example_textfield'])));
+   $this->assertText(t('form_example_checkbox has value 1'));
+   $this->assertPattern(t('/areacode.*!areacode/', array('!areacode' => $edit['form_example_element_discrete[areacode]'])));
+   $this->assertPattern(t('/prefix.*!prefix/', array('!prefix' => $edit['form_example_element_discrete[prefix]'])));
+   $this->assertPattern(t('/extension.*!extension/', array('!extension' => $edit['form_example_element_discrete[extension]'])));
+
+   $this->assertText(t('form_example_element_combined has value @value', array('@value' => $edit['form_example_element_combined[areacode]'] . $edit['form_example_element_combined[prefix]'] . $edit['form_example_element_combined[extension]'])));
+
+   // Now flip the checkbox and check for correct behavior.
+   $edit['form_example_checkbox'] = FALSE;
+   $this->drupalPost('examples/form_example/element_example', $edit, t('Submit'));
+   $this->assertText(t('form_example_checkbox has value 0'));
+ }
+
+}
Index: form_example_tutorial.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/examples/form_example/form_example_tutorial.inc,v
retrieving revision 1.8
diff -u -r1.8 form_example_tutorial.inc
--- form_example/form_example_tutorial.inc	1 Sep 2010 11:37:55 -0000	1.8
+++ form_example/form_example_tutorial.inc	3 Oct 2010 00:24:52 -0000
@@ -675,7 +675,7 @@
  * Submit function for form_example_tutorial_9().
  */
 function form_example_tutorial_9_submit($form, &$form_state) {
-  $output = t("Form 9 has been submitted. ");
+  $output = t("Form 9 has been submitted.");
   for ($i = 1; $i <= $form_state['num_names']; $i++) {
     $output .= t("@num: @first @last (@date)... ", array('@num' => $i, '@first' => $form_state['values']['name'][$i]['first'],
       '@last' =>  $form_state['values']['name'][$i]['last'], '@date' =>  $form_state['values']['name'][$i]['year_of_birth']));
--- form_example/form_example_elements.inc
+++ form_example/form_example_elements.inc
@@ -0,0 +1,451 @@
+<?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_element_info() {
+  // 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,
+    '#theme_wrappers' => array('form_element'),
+  );
+
+  $types['form_example_checkbox'] = array(
+    '#input' => TRUE,
+    '#return_value' => TRUE,
+    '#process' => array('ajax_process_form'),
+    '#theme' => 'form_example_checkbox',
+    '#theme_wrappers' => array('form_element'),
+    '#title_display' => 'after',
+
+    // form_example_checkbox 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,
+    '#theme_wrappers' => array('form_example_inline_form_element'),
+  );
+
+  // 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,
+    '#value_callback'   => 'form_example_phonenumber_combined_value',
+    '#default_value' => array(
+      'areacode' => '',
+      'prefix' => '',
+      'extension' => '',
+    ),
+    '#theme_wrappers' => array('form_example_inline_form_element'),
+  );
+  return $types;
+}
+
+
+/**
+ * Build the current combined value of the phone number only when the form
+ * builder is not processing the input.
+ *
+ * @param array $element
+ * @param boolena $input
+ * @param array $form_state
+ * @return array
+ */
+function  form_example_phonenumber_combined_value(&$element, $input = FALSE, $form_state = NULL) {
+  if (!$form_state['process_input']) {
+    $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'], $element['prefix'], $element['extension']) = $matches;
+    }
+  }
+  return $element;
+}
+
+/**
+ * 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, &$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, &$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;
+}
+
+/**
+ * Phonenumber validation function for the combined phonenumber.
+ *
+ * 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['areacode']['#value'] . $element['prefix']['#value'] . $element['extension']['#value'];
+  form_set_value($element, $value, $form_state);
+  return $element;
+}
+
+/**
+ * Called by form_example_theme() to provide hook_theme().
+ *
+ * This is kept in this file so it can be with the theme functions it presents.
+ * Otherwise it would get lonely.
+ */
+function _form_example_element_theme() {
+  return array(
+    'form_example_inline_form_element' => array(
+      'render element' => 'element',
+      'file' => 'form_example_elements.inc',
+    ),
+    'form_example_checkbox' => array(
+      'render element' => 'element',
+      'file' => 'form_example_elements.inc',
+    ),
+  );
+}
+
+/**
+ * Theme a custom checkbox.
+ *
+ * This doesn't actually do anything, but is here to show that theming can
+ * be done here.
+ */
+function theme_form_example_checkbox($variables) {
+  $element = $variables['element'];
+  return theme('checkbox', $element);
+}
+/**
+ * Format child form elements as inline elements.
+ */
+function theme_form_example_inline_form_element($variables) {
+  $element = $variables['element'];
+
+  // Add element #id for #type 'item'.
+  if (isset($element['#markup']) && !empty($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  // Add element's #type and #name as class to aid with JS/CSS selectors.
+  $attributes['class'] = array('form-item');
+  if (!empty($element['#type'])) {
+    $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-');
+  }
+  if (!empty($element['#name'])) {
+    $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
+  }
+  // Add a class for disabled elements to facilitate cross-browser styling.
+  if (!empty($element['#attributes']['disabled'])) {
+    $attributes['class'][] = 'form-disabled';
+  }
+  $output = '<div' . drupal_attributes($attributes) . '>' . "\n";
+
+  // If #title is not set, we don't display any label or required marker.
+  if (!isset($element['#title'])) {
+    $element['#title_display'] = 'none';
+  }
+  $prefix = isset($element['#field_prefix']) ? '<span class="field-prefix">' . $element['#field_prefix'] . '</span> ' : '';
+  $suffix = isset($element['#field_suffix']) ? ' <span class="field-suffix">' . $element['#field_suffix'] . '</span>' : '';
+
+  switch ($element['#title_display']) {
+    case 'before':
+      $output .= ' ' . theme('form_element_label', $variables);
+      $output .= ' ' . '<div class="container-inline">' . $prefix . $element['#children'] . $suffix . "</div>\n";
+      break;
+
+    case 'invisible':
+    case 'after':
+      $output .= ' ' . $prefix . $element['#children'] . $suffix;
+      $output .= ' ' . theme('form_element_label', $variables) . "\n";
+      break;
+
+    case 'none':
+    case 'attribute':
+      // Output no label and no required marker, only the children.
+      $output .= ' ' . $prefix . $element['#children'] . $suffix . "\n";
+      break;
+  }
+
+  if (!empty($element['#description'])) {
+    $output .= ' <div class="description">' . $element['#description'] . "</div>\n";
+  }
+
+  $output .= "</div>\n";
+
+  return $output;
+}
+
+/**
+ * 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_state) {
+  $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))));
+  }
+}


