Implementing a CTools Multi-Step Form in a Block

Implementing a CTools Multi-Step Form in a Block

No one likes long forms. They’re overwhelming to look at and it’s easy to lose your place. Multi-step forms are a way to simplify data collection and make your users’ lives easier.

Drupal’s Form API combined with the CTools module provide a solid platform for building a multi-step form. There are many wonderful guides on how to build a CTools multi-step form in Drupal. But as far as I can tell, all of the guides assume that the form will live on a page — which makes sense, as that’s the most common use case.

On a recent project, though, a client asked us to create a multi-step form using only a block, so it could be placed on a page using Panels. It turns out this is pretty straightforward but as it’s not well documented elsewhere, here’s a quick guide to what you need to do. (Note: I created an example module on GitHub, so please reference that as needed. This post is just covering the highlights.)

Update your CTools form definition

In the main CTools form definition, which looks something like this:

$form_info = array(
  'id' => 'quote-form',
  'ajax' => TRUE,
  'path' => 'example-form/%step',
  'show trail' => TRUE,
  'show back' => TRUE,
  'show return' => FALSE,

We need to change path to use query parameters for advancing the form. So let’s change path to 'path' => 'example-form?step=%step'. The path example-form could be generated by a View, a node page, a Panel page, etc.

Define the block

Next, we need an implementation of hook_block_info() and hook_block_view(). hook_block_info() is pretty unremarkable so I’m not including it here, other than to say you should consider setting cache to DRUPAL_NO_CACHE when declaring your block. Now, on to hook_block_view():

 * Implements hook_block_view().
function example_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'example_form':
      $block['subject'] = t('Our example form');
      $parameters = drupal_get_query_parameters();
      $next_step = empty($parameters['step']) ? 'step-one' : $parameters['step'];
      $block['content'] = example_ctools_wizard($next_step);
  return $block;

Let’s take a closer look at this. drupal_get_query_parameters() is checking to see if there’s a query parameter for step in the current URL (e.g. http://localhost?step=step-two). If so, we set the $next_step variable to that value; if not, we default to step-one as the starting point for the form. We then pass in the $next_step variable to our example_ctools_wizard() function, which generates the multi-step form.

Send the user on their way

So far, so good. But there’s one problem at this stage. If we try to use the form now, we’ll get errors: clicking “Continue” on the form will take you to a 404 page of http://localhost/example-form%3Fstep%3Dstep-two instead of http://localhost?step=step-two. That’s because CTools runs the path we declared in 'path' => 'example-form?step=%step' through an encoding function.

The workaround is to use our subtask_nextcallback to redirect our user where we want them to go using drupal_goto():

 * Callback executed when the 'next' button is clicked.
function example_subtask_next(&$form_state) {
  $values = (array) example_get_page_cache('quote');
  example_set_page_cache('quote', array_merge($values, $form_state['values']));
  // Because we are using query parameters to advance/rewind the form, and
  // Ctools doesn't like query parameters (URL encoding fails), we'll use
  // drupal_goto() to take the user where they need to go.
  $destination = substr($form_state['redirect'][0], strlen(example-form?step='));
  drupal_goto('example-form', array('query' => array('step' => $destination)));

The first two lines are caching form values so that as the user goes back and forth between steps on the form, their data is cached. Moving on: remember how CTools is sending us to a 404 page with the encoded value of the path we want to go to? It turns out the un-encoded value is in $form_state['redirect'][0], in the form of example-form?step=step-two.

Since we know the base path, we can use substr()andstrlen() to extract the value of step= and then pass that along to drupal_goto(). drupal_goto() bypasses CTools’ own redirection, and thus we are able to avoid the unwanted encoding of the path, and can send our users happily along their way.