Appending form elements using AHAH (Asynchronous HTML and HTTP)

Drupal 7 brings a new simplified and and clean way of handling AJAX when it comes to add dynamic form elements using the Form API.
However, using Drupal 6 to reach the same goal can be a pain in the neck sometimes specially when we use the method “append” in the AHAH request. That means we are not replacing the entire dynamic DIV with the output of the AHAH callback function but just adding new elements to the DOM.
Let’s say we have defined a simple form like this:


which in our module would translate to:


function MYMODULE_form(&$form_state) {

  $form = array();

  $form['mymodule_wrapper'] = array(
      '#type' => 'markup',
      '#value' => '',
      '#prefix' => '</pre>
<div class="ret-border">', // border wrapper
 '#suffix' => '</div>
<pre>',
  );

  $form['mymodule_wrapper']['box'] = array(
      '#type' => 'markup',
      '#value' => '',
      '#prefix' => '</pre>
<div id="box">', //ahah container
 '#suffix' => '</div>
<pre>',
  );

  $format = 'd/m/Y';

  $form['mymodule_wrapper']['box']['new-item-wrapper-0']['full-name-0']=array(
        '#type' => 'textfield',
        '#title' => t('Full name'),
        '#default_value' => '',
        '#size' => '30',
        '#prefix' =>'</pre>
<div style="height: 53px;">
<div class="full-name-wrapper">',
 '#suffix' => '</div>
',

 );

 $form['mymodule_wrapper']['box']['new-item-wrapper-0']['date-of-birth-0'] = array(
 '#type' => 'date_select',
 '#title' => t('Date of birth'),
 '#default_value' => date($format, time()),
 '#date_format' => $format,
 '#date_label_position' => 'within',
 '#date_timezone' => 'Europe/Madrid',
 '#date_increment' => 15,
 '#date_year_range' => '-80:0',
 '#prefix' =>'
<div class="date-of-birht-wrapper">',
 '#suffix' => '</div>
</div>
<pre>',
  );

  $form['mymodule_addmore'] = array(
      '#type' => 'submit',
      '#value' => t('Add new item'),
      // here is the AHAH magic
      '#ahah' => array(
          'path' => 'mymodule/ahah_callback',
          'wrapper' => 'box',
          'method' => 'append',
          'effect' => 'fade',
          'progress' => array(
              'type' => 'throbber',
              'message' => t(''),
      )),
  );

  // IMPORTANT: we will store here the number of items being added to the form
  $form['num_items'] = array('#type' => 'hidden','#default_value' => 1);

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

  return $form;
}

We have specified the AHAH callback URL as ‘mymodule/ahah_callback’ so both menu item and the proper callback function should be defined:

function mymodule_menu(){
  return array(
      'mymodule/ahah_callback' => array(
          'page callback' => 'mymodule_ahah_callback',
          'access callback' => TRUE,
          'type' => MENU_CALLBACK,
      ),

  );
}

/**
 * callback function for mymodule/ahah_callback
 * The return HTML will be outputted by AHAH
 */
function mymodule_ahah_callback() {

  // get the new items to be rendered using another helper function
  $form = mymodule_new_item_form();
  // new-item-wrapper is the div wrapper of the new elements
  // to which will be concatenated a numeric index
  $output = ahah_render($form, 'new-item-wrapper-');

  // Final rendering callback.
  print drupal_json(array('status' => TRUE, 'data' => $output));
  exit();

}

/* Helper function to add new form items */
function mymodule_new_item_form() {

  $form = array();

  // get the hidden input value with the total
  // numer of elements (minus 1). We will need to update this
  // value in the html form using Javascript. Keep on reading.
  $num_items = $_POST['num_items'];

  // num_item used as the next index
  $form['full-name-' . $num_index]=array(
      '#type' => 'textfield',
      '#title' => t('Full name'),
      '#default_value' => '',
      '#size' => '30',
      '#prefix' =>'</pre>
<div style="height: 53px;">
<div class="full-name-wrapper">',
 '#suffix' => '</div>
',
 );

 $format = 'd/m/Y';
 $form['date-of-birth-' . $num_item] = array(
 '#type' => 'date_select',
 '#title' => t('Date of birth'),
 '#default_value' => date($format, time()),
 '#date_format' => $format,
 '#date_label_position' => 'within',
 '#date_timezone' => 'Europe/Madrid',
 '#date_increment' => 15,
 '#date_year_range' => '-80:0',
 '#prefix' =>'
<div class="date-of-birth-wrapper">',
 '#suffix' => '</div>
</div>
<pre>
',
  );

  // inject JS code to increment the num_items hidden input when the form element is dynamically added
  $form['js_voodoo'] = array(
      '#type' => 'markup',
'#value' => '<script type="text/javascript">// <![CDATA[
$(document).ready(function() {$("#edit-num-items").val(parseInt($("#edit-num-items").val()) + 1)});
// ]]></script>',
 );

 return $form;

}

You might probably noted the use of the “ahah_render” function as a previous step before the final form rendering. This is where Drupal 6 is not as evolved as Drupal 7 when it comes to AJAX handling. Here is this helper function that basically gets the form from the cache and rebuilds it including the new elements added:

function ahah_render($fields, $name) {
  $form_state = array('submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  // Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
  $form = form_get_cache($form_build_id, $form_state);

  // get the num_items from the posted values and increment the stored value
  $number = $_POST['num_items'];
  $form['num_items']['#value'] =   intval($number)+1;

  // add the index to the end of the field name wrapper ("new-item-wrapper-N")
  $name .= $number;

  $form['mymodule_wrapper']['box'][$name] = $fields;

  // IMPORTANT: unset the JS before saving the form to the cache again
  // Otherwise, if the validation function fails, the JS code will be added
  // as many times as dynamic items added and the num_items counter won't be correct
  $js_voodoo = $form['mymodule_wrapper']['box'][$name]['js_voodoo'];
  unset($form['mymodule_wrapper']['box'][$name]['js_voodoo']);

  form_set_cache($form_build_id, $form, $form_state);
  $form += array(
      '#post' => $_POST,
      '#programmed' => FALSE,
  );

  // once is set to the cache, we can add it again to the form that is sent to the browser
  $form['mymodule_wrapper']['box'][$name]['js_voodoo'] = $js_voodoo;
  // And rebuild the form.
  $form = form_builder($_POST['form_id'], $form, $form_state);

  // Render the new output.
  $new_form = $form['mymodule_wrapper']['box'][$name];
  return drupal_render($new_form);
}

Using the former trick, we can safely use a validation function to assure the textfields are not empty:

function mymoudle_form_validate($form, &$form_state) {

  $number = intval($form_state['values']['num_items']);
  // this should never happen as long as the HTML form is not hacked
  if(empty($number)) {
    form_set_error('num_items', t('Unexpected error"'));
  }

  for($i=0; $i < $number; $i++) {

    $full_name = $form_state['values']['full-name-' . $i];

    if(empty($full_name)) {
      form_set_error('full-name-' . $i, t('Full name field is mandatory'));
      return;
    }

  }

}

Having a visual result of:

That is it!! Hope this helps anyone.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s