I have implemented a CakePHP multistep form. When a user clicks 'next step', data on the page is validated, saved and, if it's not the first/last step, merged with previous data in session. Besides 'next step' and 'previous step', users can also navigate between the steps that they have already passed using the navigation bar.
However, data saving and validation is only triggered by clicking 'next step'. Problems happen, for instance, when a user in step 3 clicks 'step 1' to go back and update the data, and clicks 'step 3' to return, the data will not be revised. How do I make validation work on clicking navigation links as well?
In controller (PostController):
/**
* this method is executed before starting the form and retrieves one important parameter:
* the form steps number
* you can hardcode it, but in this example we are getting it by counting the number of files that start with msf_step_
*/
public function msf_setup() {
App::uses('Folder', 'Utility');
$this->Session->delete('form');
$usersViewFolder = new Folder(APP.'View'.DS.'Posts');
$steps = count($usersViewFolder->find('msf_step_.*\.ctp'));
$this->Session->write('form.params.steps', $steps);
$this->Session->write('form.params.maxProgress', 0);
$this->redirect(array('action' => 'msf_step', 1));
}
/**
* this is the core step handling method
* it gets passed the desired step number, performs some checks to prevent smart users skipping steps
* checks fields validation, and when succeding, it saves the array in a session, merging with previous results
* if we are at last step, data is saved
* when no form data is submitted (not a POST request) it sets this->request->data to the values stored in session
*/
public function msf_step($stepNumber) {
if (null == ($this->Session->read('form.params.steps'))) {
$this->redirect(array('action' => 'msf_setup'));
}
$this->set('stepNumber', $stepNumber);
/**
* check if a view file for this step exists, otherwise redirect to index
*/
if (!file_exists(APP.'View'.DS.'Posts'.DS.'msf_step_'.$stepNumber.'.ctp')) {
$this->redirect('/posts/msf_setup');
}
/**
* determines the max allowed step (the last completed + 1)
* if choosen step is not allowed (URL manually changed) the user gets redirected
* otherwise we store the current step value in the session
*/
$maxAllowed = $this->Session->read('form.params.maxProgress') + 1;
if ($stepNumber > $maxAllowed) {
$this->redirect('/posts/msf_step/'.$maxAllowed);
} else {
$this->Session->write('form.params.currentStep', $stepNumber);
}
/**
* check if some data has been submitted via POST
* if not, sets the current data to the session data, to automatically populate previously saved fields
*/
if ($this->request->is('post')) {
/**
* if data validates we merge previous session data with submitted data, using CakePHP powerful Hash class (previously called Set)
*/
if ($this->Post->saveAll($this->request->data, array('validate' => 'only', 'deep' => true))) {
$prevSessionData = $this->Session->read('form.data');
$currentSessionData = Hash::merge( (array) $prevSessionData, $this->request->data);
/**
* if this is not the last step we replace session data with the new merged array
* update the max progress value and redirect to the next step
*/
if ($stepNumber < $this->Session->read('form.params.steps')) {
$this->Session->write('form.data', $currentSessionData);
$this->Session->write('form.params.maxProgress', $stepNumber);
$this->redirect(array('action' => 'msf_step', $stepNumber+1));
} else {
/**
* otherwise, this is the final step, so we have to save the data to the database
*/
if(AuthComponent::user('id')) {
$currentSessionData['Post']['email'] = AuthComponent::user('username');
$currentSessionData['Post']['user_id'] = AuthComponent::user('id');
unset($currentSessionData['User']); //Just in case a user is logged in after Step 1 that User data is already entered
} else {
// We can save the User data:
// it should be in $this->request->data['User']
$currentSessionData['User']['group_id'] = '4';
$user = $this->Post->User->save($currentSessionData);
// The ID of the newly created user has been set
// as $this->User->id.
$currentSessionData['Post']['email'] = $currentSessionData['User']['username'];
$currentSessionData['Post']['user_id'] = $this->Post->User->id;
unset($currentSessionData['User']);
}
$this->Post->create();
unset($this->Post->Student->validate['post_id']);
if ($this->Post->saveAssociated($currentSessionData, array('deep' => true))) {
$this->Session->setFlash(__('The post has been saved.'), 'alert_box', array('class' => 'alert-success'));
//$this->Session->delete('form');
return $this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The post could not be saved. Please, try again.'), 'alert_box', array('class' => 'alert-danger'));
}
}
}
} else {
$this->request->data = $this->Session->read('form.data');
}
/**
* here we load the proper view file, depending on the stepNumber variable passed via GET
*/
$this->render('msf_step_'.$stepNumber);
}
In the form, except for the first and last page, 'previous step', navigation and 'next page' is as follows:
//Previous Step
<?php echo $this->Html->link(__('Previous Step'),
array('action' => 'msf_step', $params['currentStep'] -1),
array('class' => 'btn btn-default')
); ?>
//navigation
<?php for ($i=1; $i <= $params['steps']; $i++) {
if ($i > $params['maxProgress'] + 1) { ?>
<a href="" class="btn btn-default" disabled><?php echo 'Step '.$i.''; ?></a>
<?php } else {
$class = ($i == $params['currentStep']) ? 'btn btn-default disabled' : 'btn btn-default';
echo $this->Html->link('Step '. $i,
array('action' => 'msf_step', $i),
array('class' => $class)
);
}
} ?>
//Next Step
<?php $options = array(
'label' => __('Next Step'),
'class' => 'btn btn-default pull-right',
'div' => array(
'class' => 'form-group'
)
);
echo $this->Form->end($options); ?>