Drupal 6: Adding form fields dynamically

There are times when you want to understand how something works, but there are plent more times when you simply want to get things done. When it came to dynamically extending Drupal 6 forms per Ajax/AHAH, I found some half-baked explanations of the "ahah voodoo" (aka implementation details) on the web, references to poll.module as the place which contains such frightening "voodoo", but no simple examples which would provide the usual delights of copy-paste reuse.

The AHAH idea is very simple - call back the server to generate a HTML snippet with a few more form fields to be inserted and subsequently submitted by client. Alas, implementing even such a basic scenario in Drupal 6 is complicated. To improve the matters a little, I implemented a Simple AHAH API module (download link). Example client code for an order form with dynamically added order items is shown below. It is almost self-explanatory (for further instructions, see included README.txt). Overall, it should reduce the implementation time for the most usual case (dynamically adding form fields) from several tiresome hours of debugging to a few minutes of thoughtless fill-in-the-blanks.

function nca_ahah_init() {
  module_load_include('inc', 'nca_ahah', 'nca_ahah');
}

function nca_ahah_menu() {
  $items['nca_ahah/nca_ahah_order_item'] = array(
    'title' => 'Javascript Order Item Form',
    'page callback' => 'nca_ahah_callback',
    'page arguments' => array('nca_ahah_order_item'),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  $items['nca_ahah/order'] = array(
    'title' => t('Order items example'),    
    'page callback' => 'drupal_get_form',
    'page arguments' => array('nca_ahah_order_form'),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  
  return $items;
}

function nca_ahah_theme() {
  return array(
    'nca_ahah_order_item' => array(
      'arguments' => array('form' => NULL),
    ),
  );
}

function nca_ahah_order_form($form_state) {
  $form = array();
  
  nca_ahah_wrapper($form, $form_state, 'nca_ahah_order_item');

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Proceed to checkout'),
    '#weight' => 2,
    );

  // Avoid submitting the form to AHAH handler on checkout. 
  $form['#action'] = url('nca_ahah/order');
  
  return $form;
}

function nca_ahah_order_form_submit($form, &$form_state) {
  if ($form_state['clicked_button']['#id'] == 'edit-submit') {
    // Real submit button was clicked.
    dpm($form_state['values']);
  }
  else {
    // "Add more" button was clicked.
  }
}

function nca_ahah_order_form_validate($form_id, &$form_state) { 
  if ($form_state['clicked_button']['#id'] == 'edit-submit') {
    // Real submit button was clicked.
  }
  else {
    // "Add more" button was clicked.
  }
}

function nca_ahah_order_item_form(&$item_form, $i, $item) {
  $item_form['item'] = array(
    '#title' => t('Item name'),
    '#type' => 'select',
    '#options' => array(0 => '', 1 => 'First Item', 2 => 'Second Item', 3 => 'Third Item'),
    '#default_value' => isset($item['item']) ? $item['item'] : 0,
    '#parents' => array('nca_ahah_order_item', $i, 'item'), // unfortunately needed
    );

  $item_form['qty'] = array(
    '#title' => t('Quantity'),
    '#type' => 'textfield',
    '#default_value' => isset($item['qty']) ? $item['qty'] : 0,
    '#size' => 5,
    '#maxlength' => 7,
    '#parents' => array('nca_ahah_order_item', $i, 'qty'), // unfortunately needed
  );
}

function theme_nca_ahah_order_item($item_form) {
  // You can do more fancy layout/HTML formatting here
  return drupal_render($item_form);
}

8 comments:

Phil said...

Great!!

Jorge said...

Grat Job, please consider to add a 'Remove' button.

Regards!

Kilian said...

how would you realize a remove button?

jpl said...

If you want a submit button for removing next to each item:

(1) Add this button's definition alongside other fields in nca_ahah_order_item_form. Set #submit to invoke a new function nca_ahah_remove_submit (defined in step 2). Other than that, the definition of this button should resemble that of the "Add more" button from nca_ahah.inc.
(2) Add the new function nca_ahah_remove_submit to nca_ahah.inc. It should look very much like nca_ahah_more_submit, but instead of increasing the counter it should mark the item-to-be-removed in $form_state.
(3) Change the definition of nca_ahah_wrapper in nca_ahah.inc to actually skip the removed item when processing $form_state and recreating the items list.

Note: if each of the submit buttons has the same label (e.g. "Remove"), you will have to give them different #name's, otherwise Drupal won't be able to distinguish which one was clicked.

Kilian said...

Thanks for ur reply!
Works fine now. I only have one problem:
If i click the add button on my edit form the added form items are prefilled with the value of the first item.
so if there already is an item in form_state every added item gets the same content...

could you point me in the right direction?

thanks so much!

jpl said...

@Kilian: What you describe doesn't happen in the original example, so it must be a problem in your code. In the example the value for each item, including newly added ones, is set by nca_ahah_order_item_form. For example, quantity is set to 0 for newly added items by this line:

'#default_value' => isset($item['qty']) ? $item['qty'] : 0,

Anonymous said...

The link for the example download is broken.

jpl said...

@Anonymous: Thanks, the link is now fixed.

Post a Comment