Ajax validation with scenarios: validation errors are not displayed on subsequent scripts

Wrote a small registration form.
Model:

    public const SCENARIO_STEP_1 = 1;
    public const SCENARIO_STEP_2 = 2;
    public const SCENARIO_STEP_3 = 3;

    public function scenarios(): array
    {
        $scenarios = parent::scenarios();
        $scenarios[self::SCENARIO_STEP_1] = ['email'];
        $scenarios[self::SCENARIO_STEP_2] = ['username', 'password'];
        $scenarios[self::SCENARIO_STEP_3] = ['firstname', 'lastname', 'organization'];

        return $scenarios;
    }
    
        public function rules(): array
    {
        return [
            ['step', 'number'],
            ['username', 'trim'],
            ['username', 'required'],
            [
                'username',
                'unique',
                'targetClass' => Users::class,
                'message'     => 'This username has already been taken.',
            ],
            ['username', 'string', 'min' => 2, 'max' => 255],

            ['email', 'trim'],
            ['email', 'required'],
            ['email', 'email'],
            ['email', 'string', 'max' => 255],
            [
                'email',
                'unique',
                'targetClass' => '\common\entity\Users',
                'message'     => 'This email address has already been taken.',
            ],

            ['password', 'required'],
            ['password', 'validatePassword'],

            [['firstname', 'lastname'], 'required'],
        ];
    }

Controller:


    $model = new SignupForm();

    $result = [
        'status' => 'success',
        'cur_step' => 0,
        'next_step' => 1,
        'final_step' => false,
    ];

    $session = Yii::$app->session;
    $session_signup_form = $session->get(self::SIGNUP_FORM);

    if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
        Yii::$app->response->format = Response::FORMAT_JSON;

        switch ($model->step) {
            case SignupForm::SCENARIO_STEP_1:
                $model->setScenario(SignupForm::SCENARIO_STEP_1);
                break;
            case SignupForm::SCENARIO_STEP_2:
                $model->setScenario(SignupForm::SCENARIO_STEP_2);
                break;
            case SignupForm::SCENARIO_STEP_3:
                $model->setScenario(SignupForm::SCENARIO_STEP_3);
                break;
            default:
                $model->setScenario(SignupForm::SCENARIO_STEP_1);
        }

        if (Yii::$app->request->get('validate')) {

            return ActiveForm::validate($model);
        }

        if ($model->validate()) {
            $session_model = new SignupForm();

            if ((int)$model->step === SignupForm::SCENARIO_STEP_1) {
                $session->set(self::SIGNUP_FORM, $model->getAttributes());
                $result['cur_step'] = SignupForm::SCENARIO_STEP_1;
                $result['next_step'] = SignupForm::SCENARIO_STEP_2;
            }

            if ((int)$model->step === SignupForm::SCENARIO_STEP_2) {
                $session_model->setAttributes($session_signup_form, false);
                $session_model->step = (int)$model->step;
                $session_model->username = $model->username;
                $session_model->password = $model->password;

                $model->setAttributes($session_signup_form, false);
                $session->set(self::SIGNUP_FORM, $session_model->getAttributes());
                $result['cur_step'] = SignupForm::SCENARIO_STEP_2;
                $result['next_step'] = SignupForm::SCENARIO_STEP_3;
            }

            if ((int)$model->step === SignupForm::SCENARIO_STEP_3) {
                $session_model->setAttributes($session_signup_form);
                $session_model->step = (int)$model->step;
                $session_model->firstname = $model->firstname;
                $session_model->lastname = $model->lastname;
                $session_model->organization = $model->organization;

                $model->setAttributes($session_model->getAttributes(), false);
                $session->set(self::SIGNUP_FORM, $session_model->getAttributes());
                $result['cur_step'] = SignupForm::SCENARIO_STEP_3;
                $result['final_step'] = true;
                if ($model->signup()) {
                    $session->remove(self::SIGNUP_FORM);
                    Yii::$app->session->setFlash(
                        'success',
                        'Thank you for registration. Please check your inbox for verification email.'
                    );
                }
            }

            return $result;
        }
    }

View:


    <?php
    $form = ActiveForm::begin([
        'id' => 'form-signup',
        'enableAjaxValidation' => true,
        'enableClientValidation' => false,
        'validateOnSubmit' => true,
        'validateOnChange' => false,
        'validateOnBlur' => false,
        'validationUrl' => Url::to(['/signup-steps', 'validate' => '1']),
        'action' => ['/signup-steps']
    ]); ?>
    <ul id="form-signup-steps" class=" uk-switcher uk-margin">
        <li>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'email', [
                    'inputOptions' => [
                        'placeholder' => 'Enter email address',
                        'type' => 'email',
                        'autocomplete' => 'email',
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-text-center">
                <a id="btn-nextstep1" class="uk-button uk-button-text uk-button-large">Next</a>
            </div>
        </li>
        <li>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'username', [
                    'inputOptions' => [
                        'placeholder' => 'Enter username',
                        'type' => 'text',
                        'autocomplete' => 'username',
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'password', [
                    'inputOptions' => [
                        'placeholder' => 'Enter password',
                        'type' => 'password',
                        'autocomplete' => 'new-password',
                        'uk-tooltip' => Yii::t('app', 'At least 8 characters of letters, numbers and special characters'),
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-text-center">
                <a id="btn-nextstep2" class="uk-button uk-button-text uk-button-large">Next</a>
            </div>
        </li>
        <li>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'firstname', [
                    'inputOptions' => [
                        'placeholder' => 'Enter firstname',
                        'type' => 'text'
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'lastname', [
                    'inputOptions' => [
                        'placeholder' => 'Enter lastname',
                        'type' => 'text'
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-margin">
                <?= $form->field($model, 'organization', [
                    'inputOptions' => [
                        'placeholder' => 'Enter organization (optional)',
                        'type' => 'text'
                    ]
                ])->label(false) ?>
            </div>
            <div class="uk-width-1-1 uk-text-center">
                <a id="btn-nextstep3" class="uk-button uk-button-primary uk-button-large"><?= Yii::t('app', 'Signup') ?></a>
            </div>
            <?= $form->field($model, 'step', ['inputOptions' => [
                'value' => SignupForm::SCENARIO_STEP_1,
            ]])->label(false)->hiddenInput() ?>
            <div hidden class="uk-width-1-1 uk-text-center">
                <button class="uk-button uk-button-primary uk-button-large">Sign up</button>
            </div>
            <div class="uk-width-1-1 uk-margin uk-text-center">
                <p class="uk-text-small uk-margin-remove">By signing up you agree to our <a class="uk-link-border" href="#">terms</a> of service.</p>
            </div>
        </li>
    </ul>
    <?php ActiveForm::end(); ?>

JS:

$form.on('afterValidate', function (event, messages, deferreds) {
        console.log(event);
        console.log(messages);
        console.log(deferreds);
        switchIconSpin();
    }).on('beforeSubmit', function (event, messages, deferreds) {
        console.log(event);
        console.log(messages);
        console.log(deferreds);
        $.ajax({
            url: $form.attr('action'),
            type: 'POST',
            data: $form.serialize(),
            /** @param {{status: string, cur_step: number, next_step: number, final_step: boolean}} data */
            success: function (data) {
                console.log(data);
               if (data) {
                    if (data.status && !data.final_step) {
                        activeCurrentStep(data.next_step)
                    }
                    if (data.final_step) {
                        activeCurrentStep(data.cur_step + 1);
                        window.location.href = '/login';
                    }
                }
            },
            error: function (jqXHR, errMsg) {
                alert(errMsg);
            }
        });
        return false; // prevent default submit
    });

The first step of registration:

The second step of registration:

What happens is the following:
We go through the first step of registration, where the script validates only the mail. Validation is correct, the error message is displayed.
Then we go to the second step of registration, where the script checks login and password. Here validation in the model is correct, but for some reason in JS in the AfterValidate event in deferreds does not return the text of error messages, which should be displayed in the form. Although in the messages parameter, these error messages are present.

How to fix it? I understand that you can handle all this manually, but I would like to use validation out of the box.

can you summarize what is not working. I’m lost in your explanations. you can add summary as separate reply

The problem is that the validation of the form by the first scenario outputs an error in the email field, validations by the following scenarios of the same form do not output errors in other fields, but they are returned as an array of errors

I will try to find time to analyze your code, but why do you disable client validation given that you seem to need it?

I only use ajax validation because I need to check for uniqueness of username. I also added additional password validation the other day with regular expression checking for password requirements and didn’t want to duplicate this functionality in js. I wanted all the checks to be in the backend

I will review your code once I settle and if no one replies.

1 Like