Implementing a CTools Multi-Step Form in a Block
Some tips on how to implement a Drupal CTools multi-step form entirely 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.)
In the main CTools form definition, which looks something like this:
<?php
$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.
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():
<?php
/**
* 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);
break;
}
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.
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():
<?php
/**
* 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.