My first tutorial! It surely won't be my last, so feedback is strongly encouraged.

Let's get started, shall we. I have added a simple module download at the end of the post to showcase the AJAX and theming (although, it doesn't actually save anything) so that you can easily have a working example to start from if you want. So feel free to skip ahead to the file download or continue with the first part of the tutorial, our form.

A Simple Drupal Form with AJAX

Let's start out with something simple, a basic form with a title element for a question and a button to add more options that will eventually allow us to add multiple answer options for our question:


//simple form function
function ea_question_form($form, &$form_state) {
$form['#tree'] = TRUE;

$form['question'] = array(
'#type' => 'textfield',
'#title' => t('Question'),
'#default_value' => '',
'#required' => TRUE,
);

$form['all_options'] = array(
'#type' => 'fieldset',
'#title' => 'Question Options',
'#prefix' => '

',
'#suffix' => '

',
);

$form['all_options']['add_option'] = array(
'#type' => 'submit',
'#name' => 'add_option',
'#value' => 'Add Option',
'#limit_validation_errors' => array(),
'#submit' => array('ea_add_option_submit'),
'#ajax' => array(
'callback' => 'ajax_ea_add_option_callback',
'wrapper' => 'ea_question_options',
),
);

return $form;
}
//callback function for our ajax form element
function ajax_ea_add_option_callback($form, &$form_state) {
return $form['all_options'];
}

If you are familiar with the Drupal Form API then everything here should be pretty standard except for a couple of things. The first thing we need to talk about is how this makes sense with the new way the FAPI handles AJAX.

Drupal 7 Changes to AJAX (Formerly AHAH)

In drupal 7, you no longer have to worry about rebuilding your form data and reprocessing form. It's all built in. This means that when you initiate an ajax call, your entire form gets processed with the new values from whatever element you clicked/changed.

So let's break this down starting on line 15 with our $form['all_options'] form element:

$form['all_options'] = array(
'#type' => 'fieldset',
'#title' => 'Question Options',
'#prefix' => '

',
'#suffix' => '

',
);

Everything is pretty standard here except for the prefix and suffix attributes. We essentially need to wrap all of our options with a div that will get replaced entirely when our ajax function is called.

Next I add a button that I will want to add options so I place the ajax attributes on it:


$form['all_options']['add_option'] = array(
'#type' => 'submit',
'#name' => 'add_option',
'#value' => 'Add Option',
'#limit_validation_errors' => array(),
'#submit' => array('ea_add_option_submit'),
'#ajax' => array(
'callback' => 'ajax_ea_add_option_callback',
'wrapper' => 'ea_question_options',
),
);

IMPORTANT - you might notice a new attribute I have added - '#limit_validation_errors' => array(), Remember, since your WHOLE form gets processed when your ajax is called, we need a way to tell the form to not check validation. This is what this attribute does. Also, this only applies to button and submit and image_button element types. If you are calling ajax on a drop down element or a checkbox, you will not need to worry about the validation issue and thus will not need this attribute. It is also important to mention that any form element values you want available in the ajax callback function you should add to the array() in the '#limit_validation_errors' attribute.

You may have noticed that I added a special submit attribute with a submit function ('#submit' => array('ea_add_option_submit')). This will be used to store how many options have been added, but we will get to that in a minute.

Personally, I also like to specify the '#name' attribute as well so that I can customize what the array key for the $form_state['values'] array will be for my button. So by setting it to 'add_option' the variable would look like this: $form_state['values']['all_options']['add_option'] (NOTE: this would look like $form_state['values']['add_option'] if i had not set $form['#tree'] = TRUE).

The final thing to note about this element is the #ajax attribute. This should be pretty self explanatory. The callback key is the name of the function that will get called when the button is clicked and the wrapper is the id of the <div> element where the returned form elements from the callback function should be rendered.

Lastly is the callback function that was specified in the $form['all_options']['add_option'] form element:


//callback function for our ajax form element
function ajax_ea_add_option_callback($form, &$form_state) {
return $form['all_options'];
}

Since all of our logic will be done in the MAIN form function, this callback becomes as simple as returning the updated form element that we want rendered. If you are a bit confused at this point, just try to remember that your entire form was processed with the value of the button element that has our ajax attribute. So if you try to run the code now, as is, you will see nothing happening because our MAIN form does not have any logic built into yet. Which leads us to our next question, how are we going to keep track of how many options we add?

Storing our options in $form_state

At this point nothing will happen when we press the "Add another option" button. Well, you should see the ajax loader .gif and some text that says "Please Wait". But other than that nothing is updated and everything will look exactly the same. So to solve this problem we are going to store some values into the $form_state variable that we can use to update our main form (remember, all the logic needs to be handled in the MAIN form function). In order to do this we are going to use that #submit callback function we specified earlier on our button. Here is the function I came up with:


function ea_add_option_submit($form, &$form_state) {
if (!empty($form_state['values']['add_option'])) {
$weight = (!empty($form_state['all_options'])) ? sizeof($form_state['all_options']) : 0;
$form_state['all_options'][] = array(
'remove' => 0,
'title' => '',
'weight' => $weight,
'option_id' => 0,
);
}
$form_state['rebuild'] = TRUE;
}

So the idea is that we check to make sure our "add option" button was clicked and then we create an array in the $form_state variable ($form_state['all_options']) to hold our info, and then push some values to that array for storing.

The last thing is just to set the $form_state['rebuild'] to true so that our $form_state['all_options'] array can be used in the main form when the AJAX callback is fired. With this submit function in place we can now add some logic to our main form function to add the options based on what is in the $form_state['all_options'] array. So in our form function we could add something like this:


//check if we have any stored values
if(!empty($form_state['all_options'])) {
$form['all_options']['options']['#theme'] = 'ea_options_theme';
foreach($form_state['all_options'] as $key => $option) {
$form['all_options']['options'][$key] = ea_option_form_el($option);
}
}

We simply check if there are any values in our $form_state['all_options'] array, and if so, we loop through them and create form elements for each one. Now, you could just create your form elements right here in the form but I chose to accomplish this by creating a separate function to hold the form elements that I would need, called ea_option_form_el($option). Which looks like this:


function ea_option_form_el($option = array()) {
$element = array(
'remove' => array(
'#type' => 'checkbox',
'#title' => '',
'#default_value' => (!empty($option['remove'])) ? $option['remove'] : '',
'#attributes' => array('class' => array('remove')),
),
'title' => array(
'#type' => 'textfield',
'#title' => '',
'#default_value' => (!empty($option['title'])) ? $option['title'] : '',
'#attributes' => array('class' => array('title')),
),
'weight' => array(
'#type' => 'hidden',
'#default_value' => (!empty($option['weight'])) ? $option['weight'] : '',
),
'option_id' => array(
'#type' => 'hidden',
'#default_value' => (!empty($option['option_id'])) ? $option['option_id'] : '',
),
);
return $element;
}

Here is a little list that explains what each field is used for or could potentially be used for:

  • Remove - Pretty self explanatory, but we could use this to flag in our submit function which options we wanted deleted
  • Title - This would hold the text for our answer option
  • Weight - Could be used if you wanted to add drag and drop sorting
  • Option ID - This would only be used when updating or deleting options from the database.

With everything in place our code should look like this now:


//simple form function
function ea_question_form($form, &$form_state) {
$form['#tree'] = TRUE;

$form['question'] = array(
'#type' => 'textfield',
'#title' => t('Question'),
'#default_value' => '',
'#required' => TRUE,
);

$form['all_options'] = array(
'#type' => 'fieldset',
'#title' => 'Question Options',
'#prefix' => '

',
'#suffix' => '

',
);

//check if we have any stored values
if(!empty($form_state['all_options'])) {
$form['all_options']['options']['#theme'] = 'ea_options_theme';
foreach($form_state['all_options'] as $key => $option) {
$form['all_options']['options'][$key] = ea_option_form_el($option);
}
}

$form['all_options']['add_option'] = array(
'#type' => 'submit',
'#name' => 'add_option',
'#value' => 'Add Option',
'#limit_validation_errors' => array(),
'#submit' => array('ea_add_option_submit'),
'#ajax' => array(
'callback' => 'ajax_ea_add_option_callback',
'wrapper' => 'ea_question_options',
),
);

return $form;
}

//submit function for our add_option button
function ea_add_option_submit($form, &$form_state) {
if (!empty($form_state['values']['add_option'])) {
$weight = (!empty($form_state['all_options'])) ? sizeof($form_state['all_options']) : 0;
$form_state['all_options'][] = array(
'remove' => 0,
'title' => '',
'weight' => $weight,
'option_id' => 0,
);
}
$form_state['rebuild'] = TRUE;
}

//callback function for our ajax form element
function ajax_ea_add_option_callback($form, &$form_state) {
return $form['all_options'];
}

//function that holds our option form elements
function ea_option_form_el($option = array()) {
$element = array(
'remove' => array(
'#type' => 'checkbox',
'#title' => '',
'#default_value' => (!empty($option['remove'])) ? $option['remove'] : '',
'#attributes' => array('class' => array('remove')),
),
'title' => array(
'#type' => 'textfield',
'#title' => '',
'#default_value' => (!empty($option['title'])) ? $option['title'] : '',
'#attributes' => array('class' => array('title')),
),
'weight' => array(
'#type' => 'hidden',
'#default_value' => (!empty($option['weight'])) ? $option['weight'] : '',
),
'option_id' => array(
'#type' => 'hidden',
'#default_value' => (!empty($option['option_id'])) ? $option['option_id'] : '',
),
);
return $element;
}

So now that everything is working we can move to last part of this tutorial. Theming!

Theming the AJAX Elements

The one thing I haven't pointed out about the above code, and it's online 21 and is pretty important for our theming stuff to work:


$form['all_options']['options']['#theme'] = 'ea_options_theme';

Now if you are familiar with how theming functions work, you will probably say that we need to set up a theme hook in our main module file before we will see anything. Well you couldn't be more right. In your module file your will need the following (make sure you replace "example_ajax" with the name of your module):


function example_ajax_theme() {
$themes = array(
'ea_options_theme' => array(
'render element' => 'form',
),
);
return $themes;
}

Now that should make theme function work. So I guess we need a theme function! Well, in my personal opinion, I think theming the options into a table would be super user friendly, so with that in mind, I put together this theming function:


function theme_ea_options_theme($variables) {
$form = $variables['form'];
$output = '';
if(!empty($form)) {
$headers = array();
$rows = array();
foreach(element_children($form) as $num => $line_items) {
$td = array();
foreach(element_children($form[$num]) as $field) {
$type = $form[$num][$field]['#type'];
if($type == 'hidden') {
$td[0]['data'] .= drupal_render($form[$num][$field]);
} else {
$headers[$field] = array('data' => $field, 'class' => array('question-option-header-'.$field));
$td[] = array('data' => drupal_render($form[$num][$field]), 'class' => array('question-option-field-'.$field));
}
}
$rows[] = $td;
}
$table = theme('table', array('header' => $headers, 'rows' => $rows));
$output .= $table;
}
$output .= drupal_render_children($form);
return $output;
}

This bit of code looks more complex than it really is and if you are familiar theming tables then you should see what is happening here. The first loop is simply going through each of the group of elements in the $form['all_options']['options'] array. The second loop is going through the form elements within each option group (remove, title, weight, option_id) and plugging them into arrays that we can use to theme a table. The last thing I do, is always call the drupal_render_children function to render any elements that I may have missed. In this case, there wont be any form elements left to render, but it is just good practice.

You, of course, don't have to theme the option into a table. I would actually recommend playing around in the theming function until you get a good idea of how it works.

If you are having trouble, I would encourage you to download the example_ajax module below. It is a simple module that showcases the code I demonstrated in this tutorial and can provide a working code base for you to troubleshoot. Remember, the module won't actually save any information, it simply demonstrates theming returned AJAX elements.

Comments

Hey,

Thanks for the tutorial. I added your plugin and wanted to know how to append another field after two. I realized that it was only possible to add one additional element.

Thank you,

Andrae

Hey Andrae,

I am not following you. Which part are you talking about? Do you want to add new fields to the "all_options" elements or are you trying to add a new set of options altogether? Perhaps you could describe what you are trying to accomplish and I can help you from there.

Thanks,

-tim