Yii multi page form wizard best practice
Asked Answered
I

2

24

I am trying to build a multi-page form with Yii, but am quite new to PHP and Yii and am wondering what the best practice is for writing a multi page form. So far, what I am planning to do is to add a hidden field named 'step' which contains the current step the user is on in the form (the form is broken into 3 steps/pages). So with that in mind, this is how I plan to handle the user clicking on previous/next buttons in the Controller:

public function actionCreate()
 {
  $userModel = new User;

  $data['activityModel'] = $activityModel; 
  $data['userModel'] = $userModel; 

  if (!empty($_POST['step']))
  {
   switch $_POST['step']:
    case '1':
     $this->render('create_step1', $data);
     break;

    case '2':
     $this->render('create_step2', $data);
     break;

  }else
  {
   $this->render('create_step1', $data);
  }
 }

Does this approach make sense? Or am I way off base and there is a much better and more optimized way of doing this in Yii/PHP?

Thanks!

Ivanaivanah answered 23/8, 2010 at 14:31 Comment(1)
Take a look at CHtml::statefulForm and a thread at Yii forumRitualist
E
36

There are a couple of ways to approach this. I see you posted in the Yii forum so I assume you've searched around there too but in case you haven't:

What I have done is (just for a simple 2-step ActiveRecord form) taken a single action and divided it up into conditional blocks based on the button name, which Yii POSTs on a form submit (note: doesn't work with ajax submits). Then, depending on which button was hit I render the correct form and set the correct scenario on my model for validation purposes.

A hidden "step" field like you have could serve the same purpose as the checking the submitButton name. I would perhaps save the "step" into the form state instead of adding a hidden field though, but either would be fine.

Some people use the stateful activeForm attribute to save the data from a single step in the wizard, or you can use the session, or even save to a temp database table. In my completely untested example below I am using a the stateful form functionality.

Here is an example of what I basically did for an ActiveRecord form. This goes in the "actionCreate":

<?php if (isset($_POST['cancel'])) {
  $this->redirect(array('home'));
} elseif (isset($_POST['step2'])) {
  $this->setPageState('step1',$_POST['Model']); // save step1 into form state
  $model=new Model('step1');
  $model->attributes = $_POST['Model'];
  if($model->validate())
    $this->render('form2',array('model'=>$model));
  else {
    $this->render('form1',array('model'=>$model));
  }
} elseif (isset($_POST['finish'])) {
  $model=new Model('finish');
  $model->attributes = $this->getPageState('step1',array()); //get the info from step 1
  $model->attributes = $_POST['Model']; // then the info from step2
  if ($model->save())
    $this->redirect(array('home'));
  else {
    $this->render('form2',array('model'=>$model));
} else { // this is the default, first time (step1)
  $model=new Model('new');
  $this->render('form1',array('model'=>$model));
} ?>

The forms would look something like this:

Form1:

<?php $form=$this->beginWidget('CActiveForm', array(
    'enableAjaxValidation'=>false,
    'id'=>'model-form',
    'stateful'=>true,
));
<!-- form1 fields go here -->
echo CHtml::submitButton("Cancel",array('name'=>'cancel');
echo CHtml::submitButton("On to Step 2 >",array('name'=>'step2');
$this->endWidget(); ?>

Form 2:

<?php $form=$this->beginWidget('CActiveForm', array(
    'enableAjaxValidation'=>false,
    'id'=>'model-form',
    'stateful'=>true,
));
<!-- form2 fields go here -->
echo CHtml::submitButton("Back to Step 1",array('name'=>'step1');
echo CHtml::submitButton("Finish",array('name'=>'finish');
$this->endWidget(); ?>

I hope that is helpful!

Equipoise answered 23/8, 2010 at 21:6 Comment(7)
Thanks that was very helpful! I quite like your approach of using the submit buttons to determine which step to display. I will also look into the ActiveForm approach, which sounds interesting.Ivanaivanah
Thanks allot. Where can i learn more stuff like that? Like professional developing with yii framework.Lorentz
regarding "note: doesn't work with ajax submits", this is true, especially in jquery, but you can just append the button name/value to the result of form.serialize and will workPorky
I've tried that by submit button name, but it never reads step2 button. When I click on step1 button it takes me to step2, when I click on step2 it skips and returns to step1.Musa
An important piece of information to make your post more complete, in that the model class must manage scenarios for validation of the form fields.Belton
wow i was searching on net for this from long time, i am sure the above answer is the best solution to divide a single form into multiple, but my problem is, i am a newbie php programmer, currently working on a project using yii2.0 framework, but i have not worked on yii 1.0 framework before , so may be my question is silly but it is complicated for me :) , my question is .. is the above syntax works with yii2.0 framework ??or is the syntax changed in yii2.0 framework for multi step form ??? please helpDirected
@Sadia Naseeba: it will surely not work with Yii 2, and even the logic may change a bit.Laural
S
4

Yii provides a feature called page states to implement things like a multi step / multi page form wizard.

Lets have a look at the Yii docs first:

A page state is a variable that is persistent across POST requests of the same page. In order to use persistent page states, the form(s) must be stateful which are generated using {@link CHtml::statefulForm}.

So the forms of every step / page need to be stateful forms. To render a stateful form you just need to set the CActiveForm::stateful property to true when you start the ActiveForm-widget. Within your controller you can get and set your page states with CController::getPageState() or CController::setPageState().

So these are the basics that work quite well if the implementation of your multi page form wizard is made in the traditional style without AJAX requests.

If however you want to use AJAX calls to submit step data and display the next step, Yii's page states are not usable.

Why? All the page states are transported through HTTP-POST within a hidden input field. The input field gets filled by Yii while the so called output processing. The output processing starts after the rendering and will replace parts of the output. So Yii's page states feature requires the output processing. AJAX responses on the other hand may become corrupted by it because the output processing may also add <link> or <script> tags at the beginning of the output to load required JS and CSS files.

In the end I implemented my own version of stateful forms. I am able to get my stateful data with the static function call ActiveFormWidget::getRequestMultiStepData() every time I need it.

Notice: There is one disadvantage in my implementation: all stateful data needs to be collected before the form widget will be initialized. But I never had a problem with it until now. However here is the code:

class ActiveFormWidget extends CActiveForm
{
    public static $inputNameMultiStepData = '_multiStepData';

    public $multiStep = false;
    public $multiStepData = array();

    public function init()
    {
        parent::init();

        # Hidden-Fields
        if ($this->multiStep) {
            echo Html::hiddenField(static::$inputNameMultiStepData, static::encodeInputData($this->multiStepData));
        }
    }

    /**
     * Gets all multi step data sent.
     * @return array|mixed
     */
    public static function getRequestMultiStepData()
    {
        return isset($_REQUEST[static::$inputNameMultiStepData]) ? static::decodeInputData($_REQUEST[static::$inputNameMultiStepData]) : array();
    }


    /**
     * Encodes form data like Yii does for stateful forms.
     * @param $data
     * @return string
     */
    public static function encodeInputData($data)
    {
        $data = Yii::app()->getSecurityManager()->hashData(serialize($data));

        return base64_encode($data);
    }

    /**
     * Decodes form data like Yii does for stateful forms.
     * @param $data
     * @return bool|mixed
     */
    public static function decodeInputData($data)
    {
        $data = base64_decode($data);
        $data = Yii::app()->getSecurityManager()->validateData($data);
        if ($data !== false) {
            return unserialize($data);
        } else {
            return false;
        }
    }
}
Spacial answered 5/9, 2014 at 8:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.