Hello,

I'm currently using the ctools form wizard. I'd like to be able to programmatically modify the order of the form steps (or remove steps) via the submit handlers. The tutorial suggests that this is possible:

Note that submit callbacks can override the order so that branching logic can be used.

However, modifying the $form_state['form_info'] values in a child-form submit function seems to have no effect on the order or presence of steps.

Is there another place that these values should be modified?

Thanks!

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

merlinofchaos’s picture

Status: Active » Fixed

I will usually store data needed in the object cache, and actually modify the form order based upon that data in the initial call to the wizard. The reason for this is that changes to the form info won't be preserved from page to page, unless you actually cache the form info itself in your object cache (which is totally a possibility if you want to do it that way), and then pass that to the wizard from your object cache, if that's how to do it.

grasmash’s picture

Status: Fixed » Active

Thanks for such a quick response. I've taken your suggestion and changed my application so that the form is built from the object cache. That part works.

However, I'm having difficultly determining exactly how the form_info should be modified in order to skip or remove steps. I saw #1570628: Documentation: How to skip one step in wizard programatically?, which suggests that modifying $form_state['clicked_button']['#next'] would correctly set the redirect for the next step, but it doesn't seem to work for me. A few other variables look tempting:

  $form_state['clicked_button']['#next']
  $form_state['triggering_element']['#next']
  $form_state['buttons'][0]['#next']
  $form_state['redirect'][0]

Anyway, the best way to communicate this may be to simply show you the code. No one likes looking through huge code blocks... but these should be fairly straightforward implementations of the form wizard.

Build form using data from object cache:

/**
 * Create callback for standard ctools registration wizard.
 */
function ysl_registration_multistep_registration_form($step = 'register') {
  ctools_include('wizard');
  ctools_include('object-cache');
  module_load_include('inc', 'ysl_registration', 'ysl_registration.ctools_wizard');

  // Grab most recent version of cached ctools object.
  $ctools_object = ysl_registration_get_ctools_cache('multistep_registration');

  // Define form_info defaults.
  if (empty($ctools_object->form_info)) {
    $ctools_object->form_info = array(
      'id' => 'multistep_registration',
      'path' => "user/register/%step",
      'show trail' => TRUE,
      'show back' => FALSE,
      'show return' => FALSE,
      'next callback' => 'ysl_registration_multistep_registration_next',
      'finish callback' => 'ysl_registration_multistep_registration_finish',
      'return callback' => 'ysl_registration_multistep_registration_finish',
      'cancel callback' => 'ysl_registration_multistep_registration_cancel',
      'order' => array(
        'register' => t('Register'),
        'location' => t('Set your location'),
        'friends' => t('Find your friends'),
        'groups' => t('Connect'),
      ),
      'forms' => array(
        'register' => array(
          'form id' => 'user_register_form',
        ),
        'location' => array(
          'form id' => 'ysl_registration_location_form',
          'include' => drupal_get_path('module', 'ysl_registration') . '/ysl_registration_location_form.inc',
        ),      
        'friends' => array(
          'form id' => 'ysl_registration_friends_form',
          'include' => drupal_get_path('module', 'ysl_registration') . '/ysl_registration_friends_form.inc',
        ),
        'groups' => array(
          // The title for this is set in preprocess-page.inc
          'form id' => 'ysl_registration_group_info_form',
          'include' => drupal_get_path('module', 'ysl_registration') . '/ysl_registration_groups_form.inc',
        ),
      ),
    );
  }
  $form_state['ctools_object'] = $ctools_object; 
  $output = ctools_wizard_multistep_form($ctools_object ->form_info, $step, $form_state);

  return $output;
}

In my custom submit handler for step 1 (user_register_form), modify the form_info in the object cache:

/**
 * Submit callback for user_register_form.
 */
function ysl_registration_user_register_form_submit($form, &$form_state) {
  // Add postal code to signup object. Object is cached via ctools wizard.
  $form_state['ctools_object']->data['postal_code'] = $form_state['values']['postal_code'];
  
  // We're going to remove the 'location' step.
  unset($form_state['ctools_object']->form_info['order']['location']);
  unset($form_state['ctools_object']->form_info['forms']['location']);
  
  // Determine what the next step should be.
  $steps = $form_state['ctools_object']->form_info['order'];
  $current = $form_state['step'];
  while (key($steps) !== $current) next($steps);
  next($steps);
  $next = key($steps);

  // Set the next step.
  $form_state['clicked_button']['#next'] = $next;
  $form_state['triggering_element']['#next'] = $next;
  $form_state['next'] = $next;
}

A few utility functions to handle the object cache:

/**
 * Callback generated when the 'next' button is clicked.
 *
 * All we do here is store the cache.
 */
function ysl_registration_multistep_registration_next(&$form_state) {
  // Update the cache with changes.
  ysl_registration_set_ctools_cache('multistep_registration', $form_state['ctools_object']);
}

/**
 * Get the cached changes to a given task handler.
 */
function ysl_registration_get_ctools_cache($name) {
  ctools_include('object-cache');
  $cache = ctools_object_cache_get('ysl_registration', $name);
  if (!$cache) {
    $cache = new stdClass();
    $cache->locked = ctools_object_cache_test('ysl_registration', $name);
  }
  return $cache;
}

/**
 * Store changes to a task handler in the object cache.
 */
function ysl_registration_set_ctools_cache($name, $data) {
  ctools_include('object-cache');
  $cache = ctools_object_cache_set('ysl_registration', $name, $data);
}

/**
 * Remove an item from the object cache.
 */
function ysl_registration_clear_ctools_cache($name) {
  ctools_include('object-cache');
  ctools_object_cache_clear('ysl_registration', $name);
}

Unfortunately, the 'location' step is still set as step 2 after this modification. Unsetting the 'order' key merely removes the step from the form trail, and removing the 'forms' key will result in a blank page. Regardless, I am still redirected to the original step 2 url.

Thanks in advance!

grasmash’s picture

Status: Active » Fixed

Managed to figure this out! The issue was that the form had multiple submit handlers, and they weren't being called in the right order. After defining the correct order, everything seems to work.

    // It is very important that these submit handlers fire in the right order!
    $form['#submit'] = array(
      'user_register_submit',
      'ysl_registration_user_register_form_submit',
      'ctools_wizard_submit',
    );

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

AritoMelo’s picture

Hi guys!

I need to change the step order for BACK button as u guys did for NEXTso far I can't get it working.
I've already did for NEXT btt, and it works.
Any idea?

medienverbinder’s picture

Issue summary: View changes

Hi aritomelo!

You could use the next callback function (The function to call when the next button has been clicked) and fiddle "$form_state['redirect']"

Example:

/**
 * Handles the 'next' click on the add/edit pane form wizard.
 *
 * All we need to do is store the updated pane in the cache.
 */
function your_wizard_next_callback(&$form_state) {

    ...

    if($form_state['triggering_element']['#value'] == "Back") {
        $form_state['redirect'] = 'PATH/TO/YOUR/FORM/STEP/X';
    }

    ...
}
IRuslan’s picture

IRuslan’s picture

Status: Closed (fixed) » Needs review
FileSize
583 bytes

I have a multistep custom form.
Based on a value of radio button on the first step, I need to turn on or off step 3.

If I made it in submit, this values will be lost.
Therefore I find out it could be good to make possible to modify $form_info within one of the form_submits and pass it to further steps.
Currently it's not possible because in ctools_wizard_multistep_form() we have separate variable $form_info, and changes in $form_state['form_info'] will be reflected for internal call of ctools_wizard_multistep_form().

That's why I propose to update $form_info values from $form_state['form_info'] after:
$output = drupal_build_form($info['form id'], $form_state);
string.

With this correction, it's possible to do stuff like that:

function step_1_form_submit(array $form, array &$form_state) {
  if (!empty($form_state['values']['use_step_3'][LANGUAGE_NONE][0]['value'])) {
    // Add step 3 as last element of steps.
    $form_state['form_info']['order']['step_3'] = '';
  }
  // Or if we changed type, remove extra step if it existed.
  elseif (isset($form_state['form_info']['order']['step_3'])) {
    unset($form_state['form_info']['order']['step_3']);
  }
}

Comparing to the current approach, it's easy to control a whole state of form_info, instead of manipulating with next or previous steps.

See patch in attachment.

Chris Matthews’s picture

Version: 7.x-1.1 » 7.x-1.x-dev

The 2 year old patch in #8 to wizard.inc applied cleanly to the latest ctools 7.x-1.x-dev, but still needs to be reviewed and tested.

MustangGB’s picture

Category: Support request » Feature request