In multi-step forms, when i refresh the third step, it takes me to the first step. What might be the reason?

Comments

BayerMeister’s picture

Hi. I read through so much to find what I'm doing wrong, but nothing found. This is just the problem I have. If I refresh on a multi-step form somewhere ... like in the 3rd step (will show example below), I get somewhere else.

I've made a demonstration test_module which does just show this effect. I'd be glad if someone pointed out, what the reason for the form's behavior is.

Here's the code. I'm sorry it's longer. It reflects the structure I want to achieve. Comments are there throughout the code. If it worked it could be a nice example how-to for multi-steps.

Problem description: Form works OK except for (F5) refreshes. You have the 'Main' form from where you can get to 'Primes' and 'Fibonacci'. OK until now. If you press 'Next' on those pages, you get to the next Prime or Fibonacci number. Refresh gets you to the first prime number from here on (from the second, third... Prime or Fibbonacci number). The form does handle well on the Main and the first Prime and Fib. number.

This can be made even simpler without the two branches. The problem stays. F5 (refresh) fires the submit handler with (to me) bad data.

//hook_menu to access this test at /test_page
function test_module_menu() {
  return array('test_page' => array(
    'title' => 'Multi-step test',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('_test_module_form'),
    'access callback' => TRUE,
  ));
}

//The form builder switches to sub-routines according to the content of $form_state['storage']['action']
//At the beginning the action is not defined and so the "default:" applies
function _test_module_form($form_state) {
  switch($form_state['storage']['action']) {
    case 'Primes':
      return _tmf_Primes($form_state);
    case 'Fibonacci':
      return _tmf_Fibonacci($form_state);
    default:
      return _tmf_Main($form_state);
  }
}

//The form submit switches just as the builder, nothing surprising here
function _test_module_form_submit($form, &$form_state) {
  switch($form_state['storage']['action']) {
    case 'Primes':
      _tmfs_Primes($form, $form_state);
      break;
    case 'Fibonacci':
      _tmfs_Fibonacci($form, $form_state);
      break;
    default:
      _tmfs_Main($form, $form_state);
      break;
  }
}

//The "default:" builder offers two buttons to navigate to the other two "sub-forms" below
function _tmf_Main($form_state) {
  $form['GoToPrimes'] = array(
    '#type' => 'submit',
    '#value' => 'Go to Primes',
  );
  $form['GoToFibonacci'] = array(
    '#type' => 'submit',
    '#value' => 'Go to Fibonacci',
  );
  return $form;
}

//This form is displayed when the ['action'] in storage was set to 'Primes' adn it just
//displays the first number from $form_state['storage']['Primes-DATA'] or a depleted message
//Two buttons - next and cancel (see submit handler)
function _tmf_Primes($form_state) {
  if (count($form_state['storage']['Primes-DATA'])) {
    $form['info'] = array(
      '#value' => '<p>Prime number is: '.reset($form_state['storage']['Primes-DATA']).'</p>',
    );
    $form['Next'] = array(
      '#type' => 'submit',
      '#value' => 'Next',
    );
  }
  else $form['info'] = array(
    '#value' => '<p>Prime numbers depleted</p>',
  );

  $form['Cancel'] = array(
    '#type' => 'submit',
    '#value' => 'Cancel',
  );
  return $form;
}

//Just the same for Fibonacci as for Primes in the previous
function _tmf_Fibonacci($form_state) {
  if (count($form_state['storage']['Fibonacci-DATA'])) {
    $form['info'] = array(
      '#value' => '<p>Fibonacci number is: '.reset($form_state['storage']['Fibonacci-DATA']).'</p>',
    );
    $form['Next'] = array(
      '#type' => 'submit',
      '#value' => 'Next',
    );
  }
  else $form['info'] = array(
    '#value' => '<p>Fibonacci numbers depleted</p>',
  );

  $form['Cancel'] = array(
    '#type' => 'submit',
    '#value' => 'Cancel',
  );
  return $form;
}

//The 'default:' submit handler sets the action according to the button clicked
//and prepares data
function _tmfs_Main($form, &$form_state) {
  switch($form_state['clicked_button']['#id']) {
    case 'edit-GoToPrimes':
      $form_state['storage']['action'] = 'Primes';
      $form_state['storage']['Primes-DATA'] = array(2,3,5,7,11,13,17,19,23);
      break;
    case 'edit-GoToFibonacci':
      $form_state['storage']['action'] = 'Fibonacci';
      $form_state['storage']['Fibonacci-DATA'] = array(1,2,3,5,8,13,21,34,55,89);
      break;
  }
}

//The submit handler for the Primes "sub-form". If next pressed, the data is shifted,
//Cancel clears ['storage'] and brings us thereby back to "default:" again
function _tmfs_Primes($form, &$form_state) {
  switch($form_state['clicked_button']['#id']) {
    case 'edit-Next':
      array_shift($form_state['storage']['Primes-DATA']);
      break;
    case 'edit-Cancel':
      unset($form_state['storage']);
      break;
  }
}

//same as above for Fibonacci
function _tmfs_Fibonacci($form, &$form_state) {
  switch($form_state['clicked_button']['#id']) {
    case 'edit-Next':
      array_shift($form_state['storage']['Fibonacci-DATA']);
      break;
    case 'edit-Cancel':
      unset($form_state['storage']);
      break;
  }
}

If this get's fixed, I'll make it a tutorial, I guess. Spent some time with it already...
Thanks for reading and for advice!

BayerMeister’s picture

This shows the problem as described in the original post by newtodrupal on May 17, 2009
You can refresh on the 1st and 2nd page without visible problems. A refresh on any further page (3rd, 4th, ...) takes you to the 2nd.
This would seem like $form_state did freeze in step 2, but the more complex example above showed, that we are taken just "somewhere else" without "logic".

//+ hook_menu UNCHANGED

//The builder shows a button with "0++". The number comes from form_state['storage'] which gets populated in the submit handler
function _test_module_form($form_state) {
  $num = (int)$form_state['storage']['number'];
  $form['Next'] = array(
    '#type' => 'submit',
    '#value' => $num.'++',
  );
  return $form;
}

//submit handler increments $form_state['storage']['number']
function _test_module_form_submit($form, &$form_state) {
  if ($form_state['clicked_button']['#id'] == 'edit-Next')
    $form_state['storage']['number']++;
  //$form_state['rebuild'] is set automatically when ['storage'] is populated... I hope, see http://drupal.org/node/144132#form-state
}
rjcyu’s picture

Just in case anyone is looking for a solution to this, ran into this problem with D7. Form some reason, the "form_build_id" was being changed after refresh in third step, so my solution was to store the "form_build_id" in storage after step 2, and restore it in step 3. My preliminary tests showed that after this, a refresh in step 3 retained values.

petew’s picture

I'm seeing exactly the same behaviour in my multi-step forms. It doesn't seem there's a solution anywhere for allowing/catering for page refreshing within multi-step forms.

As far as I can tell the form_build_id is being regenerated on each new form build for each step of the multi-step form. (Turning on AHAH on all buttons might avoid a normal form submission as it seems an AHAH submission does keep the form_build_id for the current form, so that might work but i've not yet tried it as I really wanted to get this working without AHAH first)

If you've got any examples of an easy way of setting/overriding the form_build_id to your own value that would be most useful. I'm currently trying to get this working in D6 but if you have this working in D7 that might provide some pointers. (Are you storing the value in the form_state['storage'] during the first submit handler call? It's where/when to set the $form['#build_id'] and/or $form['form_build_id'] that I haven't quite figured out. I tried in the form function called by drupal_get_form but it seems to just get ignored and a new value generated)

Any help or pointers would be gratefully received.

akki123’s picture

$form_state lose all its value after second step on page refresh.

If 'step' is the variable set in $form_state and user is on third step and refresh the page, form redirects to second step and losing 'step' value.

// 'step' is set in formSubmit
$form_state->set('step', 1);

// get 'step' in formBuild 
$form_state->get('step');

$form_state->isCached() returns TRUE on third step and is the reason of lost 'step' value.

There is various $form_state functions for Drupal 8 version

https://www.drupal.org/node/2310411

So I used this to disable that cache - $form_state->setCached(FALSE) before $form_state_setRebuild(). So it becomes

$form_state->setCached(FALSE);
$form_state->setRebuild();

akki

HbtTundar’s picture

I wanna create a form builder in drupal 8.7 

so i create a Masterclass to render all forms that create base on this master class the problem is, when I create a subclass based on this master class, on the second step of each subforms that extends from the master class the form_state reset and i can not handle the next step in multistep form after the second step. is there anybody whom know what's the problem and why the form_state reset .

i also enable ajax for my forms 

arif.zisu’s picture

I am facing some issue at D7 multistep form. After update PHP version 7.x to 8.x.
Please help me to provide some solution.

When I submit step 2 , it will not go to step2_form_submit . It always submit step1_form_submit.

jaypan’s picture

Multistep forms cannot be refreshed. This is a bug in Drupal. If refreshed, the user is returned to the first step.

Contact me to contract me for D7 -> D10/11 migrations.

arif.zisu’s picture

Hi,

This form was working as expected at php 7.4 version , but issue at php 8.0 . I am trying to explain issue in details.

function mymodule_main_form($form, &$form_state)

{

// Check to see if anything has been stored.

if ($form_state['rebuild']) {

$form_state['input'] = array();

}

if (empty($form_state['storage'])) {

// No step has been set so start with the first.

$form_state['storage'] = array(

'step' => 'mymodule_step1_form',

);

}

// Return the current form

$function = $form_state['storage']['step'];

$form = $function($form, $form_state);

return $form;

}

function mymodule_main_form_submit($form, &$form_state)

{

$values = $form_state['values'];

if (isset($values['cancel']) && $values['op'] == $values['cancel']) {

// Moving back in form.

$step = $form_state['storage']['step'];

// Call current step submit handler if it exists to unset step form data.

if (function_exists($step . '_submit')) {

$function = $step . '_submit';

$function($form, $form_state);

}

// Remove the last saved step so we use it next.

$last_step = array_pop($form_state['storage']['steps']);

$form_state['storage']['step'] = $last_step;

} else {

// Record step.

$step = $form_state['storage']['step'];

$form_state['storage']['steps'][] = $step;

// Call step submit handler if it exists.

if (function_exists($step . '_submit')) {

$function = $step . '_submit';

$function($form, $form_state);

}

}

return;

}

function mymodule_step1_form($form, &$form_state)

{

if (!empty($form_state['storage'])) {

$values = $form_state['storage'];

}

$form['test1'] = array(

'#type' => 'textfield',

'#title' => t('test1'),

'#default_value' => isset($values['test1']) ? $values['test1'] : '',

);

$form['submit'] = array(

'#type' => 'submit',

'#value' => t('Activate'),

'#attributes' => array('class' => array('test_class')),

);

return $form;

}

function mymodule_step1_form_submit($form, &$form_state)

{

try {

$values = $form_state['values'];

$form_state['rebuild'] = TRUE;

$form_state['storage']['test1'] = $form_state['values']['test1'];

$form_state['storage']['step'] = 'mymodule_step2_form';

} catch (Exception $e) {

watchdog('test_error', 'Error: "!err"', array('!err' => $e->getMessage()));

}

}

function mymodule_step2_form($form, &$form_state)

{

$form['submit'] = array(

'#type' => 'submit',

'#value' => t('Confirm'),

'#attributes' => array('class' => array('test_class')),

);

$form['cancel'] = array(

'#type' => 'submit',

'#prefix' => '&nbsp;',

'#value' => t('Cancel'),

'#submit' => array('mymodule_step2_form_submit'),

'#attributes' => array('class' => array('test_class')),

);

return $form;

}

function mymodule_step2_form_submit($form, &$form_state)

{

$values = $form_state['values'];

print_r($values);

}

Inside some block we are rendering  mymodule_main_form

Like

$block = new stdClass();

$content = drupal_get_form('mymodule_main_form');

    if ($content) {

        $block->content = $content;

    }

return $block;    

So as per code it will render mymodule_step1_form as default  $form_state['storage']['step'] = 'mymodule_step1_form'

When we submit this form if will call mymodule_main_form_submit and it will go to the else part. Call mymodule_step1_form_submit.

Inside mymodule_step1_form_submit form_state rebuild true and set step  $form_state['storage']['step'] = 'mymodule_step2_form'

So it will go to again mymodule_main_form and render mymodule_step2_form.

After that if I click Confirm it will call mymodule_main_form_submit but  $step / $form_state['storage']['step'] still mymodule_step1_form. It will not update with mymodule_step2_form , So not able to call mymodule_step2_form_submit.

If I tigger cancel button , same issue. Also not getting any Error / Fatal error at Watchdog related to this form 

Same form work for php7.