Skip to main content

Using ajax callbacks on field instance settings forms.

Drupal's hook_field_instance_settings_form, hook_field_settings_form and hook_field_widget_settings_form are invoked without being passed the $form_state variable. This means you cannot perform ajax callbacks because you cannot access the current form state...... or can you?

by lee.rowlands /

Background

Drupal has an awesome Ajax api that lets you perform complex javascript/ajax operations using php. Basically these allow you to attach ajax behaviours to your form elements to allow events such as a button click or a select change to fire an ajax callback and rebuild a portion of the form. To find out more about adding ajax callbacks, see the api reference. Typically these involve inspecting the current form values in $form_state['values'] to change the way in which the form is built.

However if you are building a field type or field widget, you cannot access easily $form_state in your form builder callback because it is not passed.

There is an issue filed to resolve this, although in Drupal 8 the conversion of widgets and field types to plugins will most likely see this resolved, meaning we should only have to backport the patch to Drupal 7 once these land. But how do we get around it in the meantime? Without patching core? On first inspection it didn't seem possible, so I focussed my attention on writing the patch for the issue, but then I happened to be using the Entity Reference project and noticed ajax behaviours on the instance settings form. What the? Is this some kind of black magic? No it's just another cool feature of Drupal's form api, process callbacks.

Adding ajax behaviours using process callbacks

Taking the example of Entity Reference, instead of building the settings form in hook_field_settings_form, it just generates a simple container element with a process callback.

/**
 * Implements hook_field_settings_form().
 */
function entityreference_field_settings_form($field, $instance, $has_data) {
  // The field settings infrastructure is not AJAX enabled by default,
  // because it doesn't pass over the $form_state.
  // Build the whole form into a #process in which we actually have access
  // to the form state.
  $form = array(
    '#type' => 'container',
    '#process' => array('_entityreference_field_settings_process'),
    '#element_validate' => array('_entityreference_field_settings_validate'),
    '#field' => $field,
    '#instance' => $instance,
    '#has_data' => $has_data,
  );
  return $form;
}

This takes advantage of the fact that process callbacks get given $element and $form_state as arguments. So all the ajax behaviours and building the form based on the current values takes place in the process callback instead

function _entityreference_field_settings_process($element, $form_state) {
  //... update the element as required using $form_state!
  // Be sure to return the element.
  return $element;
}

It is worth pointing out how Entity reference merges the settings with the form state so that the $field or $instance is always consistent. This is done in an element validate callback.

function _entityreference_field_settings_validate($form, &$form_state) {
  // Store the new values in the form state.
  $field = $form['#field'];

  if (isset($form_state['values']['field'])) {
    $field = drupal_array_merge_deep($field, $form_state['values']['field']);
  }
  $form_state['entityreference']['field'] = $field;
}

Here entity reference uses drupal_array_merge_deep to ensure that the $form_state values are applied to the $field array and then stores it in $form_state so the process callback has access to the most up to date version of the $field, taking into account recent unsaved changes made by the user and sent to the server with ajax.

Pretty cool hey?

Summary

Drupal's form api is full of hidden gems - what are some of your favourites?

Posted by lee.rowlands
Senior Drupal Developer

Dated

Comments

Comment by Vijay

Dated

Definitely, the usefulness dneepds on what you need to do; this is no sonic screwdriver. A few use cases I can think of:1. Forms (e.g., a registration form, a cart/checkout form, a send a letter to your congressman form, etc.). Some forms make sense to store in posts, other don't. For the latter, you need a way to include the form on the page, and process the results, and this plugin makes that easier.2. APIs (especially of the RESTful variety). This makes it easy to set up URL endpoints, and pass arguments from the URL string to a callback function. 3. Displaying external data. Say you're pulling data off of an external API, and you want to generate reports or dynamic content based on that data. You won't have any posts in your database corresponding to this external data, but you need to call a function to display it.In general, if you want to display a post, or a list of posts, this plugin is not the answer (although it could be horribly abused to do so). If you want to display something that is not a post, though, this provides an easy way to do it that was missing in WordPress.

Pagination