Custom in-browser field state changing on Drupal API-built forms

Let's say you've built, or are building, a form in Drupal 7, using Form API. You want certain fields to change, depending on certain other fields: for example, if someone clicks a checkbox, you want other fields to disable because they're irrelevant. This must all happen in the browser.

Core D7: form states to modify a form in the browser

This is what form states were intended for: they allow you to use Form API array attributes to:

  1. listen to another element (using jQuery selectors)
  2. wait for the element to match certain conditions (e.g. checked/unchecked)
  3. if the conditions are met, perform an action the element whose Form API array you've added to

This works a treat, but unfortunately the number of different condition types (2) and action types (3) available in core is limited. But the great news is you can extend them if you need to.

Extending core form states with a custom action

We were building an install profile intended to build a site with "one click": or at any rate, trying to reduce the number of clicks etc. as much as possible. One thing we wanted, post-install, was to be able to create a UID=2 user very quickly. This is important, as UID=1 bypasses the permissions system, and if everyone has to keep using UID=1 then there's no paper trail: we wanted instead to have a team of admins on the site, all under their real names, all with configurable permissions.

However, while we were developing, and repeatedly reinstalling, we didn't want to have to keep entering in the details for UID=2. Could we have it so that, if a checkbox for "dev mode" is checked, then the form for UID=2 is automatically pre-populated?

It turns out that it's easy to extend form states to have this new "devmode" action, using the following Javascript:

/**
 * Support post-install task forms.
 */
(function($) {
 
// Username, email suffix, password etc. in dev mode.
var username = "site.admin";
 
/**
 * Register custom state change: devmode.
 */
$(document).bind('state:devmode', function(e) {
  if (e.trigger) {
    // If we're in dev mode, populate fields.
    if (e.value) {
      $('#edit-username').val(username);
      $('#edit-email').val(username + "@example.com");
      $('#edit-password-pass1').val(username);
      $('#edit-password-pass2').val(username);
    }
    // If we're not in dev mode, ensure fields are empty.
    else {
      $('#edit-username').val("");
      $('#edit-email').val("");
      $('#edit-password-pass1').val("");
      $('#edit-password-pass2').val("");
    }
  }
});
 
})(jQuery);

Put this in the same module you're building your form with. Now to use the extension!

Using our extension to the form state actions

To use this extension action on a particular form, you need to do two things:

  1. Attach the new Javascript file to the form using an #attach parameter.
  2. Register a form state change, which uses the action.

Here's how you might do this in a form API callback:

<?php
/**
 * Form API callback: implements state:devmode.
 */
function mymodule_myform($form, &$form_state) {
  /* ... */
 
  // We only add the state change to one field; JS references the others.
  $form['users']['new_user']['username']['#states'] = array(
    // Set a watch for our checkbox's element ID: edit-production-mode.
    'devmode' => array('#edit-production-mode' => array('unchecked' => TRUE)),
  );
 
  // And attach Javascript we wrote earlier, to support our new action.
  $form['#attached']['js'][] = drupal_get_path('module', 'mymodule')
    . '/js/devmode.js';
 
  /* ... */
  return $form;
}

And that's it. Custom form state action, implemented in around two dozen lines of code (many of them comments). Obviously anything serious you would do with form states, you'd also want to check for on the server: data integrity, etc. But for improving the user experience in small increments, form states take a lot of beating.