Active form suddenly submits via ajax

I have had this form for some days now, some fields use ajax validation. As production is near, today I started testing this form again and to my surprise it is submitting via ajax instead of normal post request.

view file:

<section class="bg-light p-4">
    <h1><?= Html::encode($this->title) ?></h1>

    <p><?= Yii::t('app', 'p_signup') ?>:</p>

    <div class="row">
        <div class="col-lg-5">
            <?php $form = ActiveForm::begin(); ?>

                <?= $form->field($model, 'type')->radioList([0 => 'Particular', 1 => 'Company'], ['class' => 'type']) ?>
                <?= $form->field($model, 'name', ['enableAjaxValidation' => true])->textInput() ?>
                <?= $form->field($model, 'company', ['enableAjaxValidation' => true])->textInput() ?>
                <?php if (Yii::$app->user->can('createStaff')) {
                    echo $form->field($model, 'username', ['enableAjaxValidation' => true])->textInput();
                } ?>
                <?= $form->field($model, 'email', ['enableAjaxValidation' => true])->input('email', ['autoComplete' => 'off']) ?>
                <input type="text" disabled hidden>
                <?= $form->field($model, 'password')->passwordInput(['autocomplete' => 'off']) ?>
                <?= $form->field($model, 'confirm_password')->passwordInput(['autocomplete' => 'off']) ?>

                <div class="form-group">
                    <?= Html::submitButton(Yii::t('form', 'b_signup'), ['class' => 'btn btn-primary', 'name' => 'signup-button']) ?>
                </div>

            <?php ActiveForm::end(); ?>
        </div>
    </div>
</section>

Controller action:

public function actionSignup()
{
    $model = new SignupForm();
    $post = Yii::$app->request->post();

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

    if ($model->load($post) && $model->signup()) {
        Yii::$app->session->setFlash('success', 'Thank you for registration. Please check your inbox for verification email.');
        return $this->goHome();
    }

    return $this->render('signup', [
        'model' => $model
    ]);
}

The culprit seems to be Yii client-side scripts:

send
https://static.example.com/www/assets/96c771a0/jquery.js:10099:10
ajax
https://static.example.com/www/assets/96c771a0/jquery.js:9682:15
validate/<
https://static.example.com/www/assets/abd67956/yii.activeForm.js:379:23
fire
https://static.example.com/www/assets/96c771a0/jquery.js:3496:31
add
https://static.example.com/www/assets/96c771a0/jquery.js:3555:7
always
https://static.example.com/www/assets/96c771a0/jquery.js:3708:15
validate
https://static.example.com/www/assets/abd67956/yii.activeForm.js:366:43
submitForm
https://static.example.com/www/assets/abd67956/yii.activeForm.js:441:34
dispatch
https://static.example.com/www/assets/96c771a0/jquery.js:5429:27
add/elemData.handle
https://static.example.com/www/assets/96c771a0/jquery.js:5233:28
(Async: EventListener.handleEvent) add
https://static.example.com/www/assets/96c771a0/jquery.js:5281:12
on/<
https://static.example.com/www/assets/96c771a0/jquery.js:5181:16
each
https://static.example.com/www/assets/96c771a0/jquery.js:381:19
each
https://static.example.com/www/assets/96c771a0/jquery.js:203:17
on
https://static.example.com/www/assets/96c771a0/jquery.js:5180:14
on
https://static.example.com/www/assets/96c771a0/jquery.js:5920:10
init/<
https://static.example.com/www/assets/abd67956/yii.activeForm.js:232:27
each
https://static.example.com/www/assets/96c771a0/jquery.js:381:19
each
https://static.example.com/www/assets/96c771a0/jquery.js:203:17
init
https://static.example.com/www/assets/abd67956/yii.activeForm.js:198:25
$.fn.yiiActiveForm
https://static.example.com/www/assets/abd67956/yii.activeForm.js:18:33
<anonymous>
https://www.example.com/user/signup:531:15
mightThrow
https://static.example.com/www/assets/96c771a0/jquery.js:3762:29
resolve/</process<
https://static.example.com/www/assets/96c771a0/jquery.js:3830:12
(Async: setTimeout handler) resolve/<
https://static.example.com/www/assets/96c771a0/jquery.js:3868:16
fire
https://static.example.com/www/assets/96c771a0/jquery.js:3496:31
fireWith
https://static.example.com/www/assets/96c771a0/jquery.js:3626:7
fire
https://static.example.com/www/assets/96c771a0/jquery.js:3634:10
fire
https://static.example.com/www/assets/96c771a0/jquery.js:3496:31
fireWith
https://static.example.com/www/assets/96c771a0/jquery.js:3626:7
ready
https://static.example.com/www/assets/96c771a0/jquery.js:4106:13
completed
https://static.example.com/www/assets/96c771a0/jquery.js:4116:9
(Async: EventListener.handleEvent) <anonymous>
https://static.example.com/www/assets/96c771a0/jquery.js:4132:11
<anonymous>
https://static.example.com/www/assets/96c771a0/jquery.js:36:10
<anonymous>
https://static.example.com/www/assets/96c771a0/jquery.js:40:4

Can I get some help? Thanks.

Edit: I’m pretty sure this is caused by some changes in the last update, as all forms are now sent via ajax. The problem is, when you’re using ajax validation this becomes a problem cuz it makes it impossible to process the request.

Not sure I understand your question. You have 'enableAjaxValidation' => true in a bunch of places, for some fields, so why would it not submit validation by AJAX?

Regardless of that, can’t you just revert back to the last commit that you know did not have this problem, and verify that it does indeed work the way you expect there, and then bisect your way forward?

I mean when I submit the form, not when the input event triggers the ajax request to validate the data.

The problem is when I submit the form by either clicking the button or pressing enter, the first if evaluates to true since it’s an ajax request, therefore instead of processing the signup(), it returns a json with validation data.

Btw, the stacktrace is from the submit event, not from the data validation.

I see what you mean now.

Regardless; If you did not have this problem/behavior a while back, and you have it now, bisect your code to find where something changed.

I’ve been looking for a while now, I can’t find anywhere in the code I wrote nothing about Yii active form attaching a submit event to all my forms.

Even the contact form which is the one from the advanced template, minus some fields, it is the same and I can see a submit event attached.

That’s not what I mean. You have your application’s code version controlled using Git or similar, don’t you? If you don’t, you really should be :stuck_out_tongue:

If you do, you check out the last commit that you know did not show the problem, run the app on that version/commit to verify the problem isn’t there, and then you start trying later commits to see at which point the problem appears. Then you know at which commit this problem/regression started happening.

Ok I’ve found the problem, it has to do with required/optional attributes. The problem is the rule doesn’t seem to be working:

[['type'], 'required'],
[['type'], 'integer'],
[['type'], 'in', 'range' => [self::TYPE_PARTICULAR, self::TYPE_COMPANY]],

[['name'], 'required', 'when' => function($model) { return $model->type === self::TYPE_PARTICULAR; }],
[['company'], 'required', 'when' => function($model) { return $model->type === self::TYPE_COMPANY; }],
[['name', 'company'], 'string', 'max' => 50],

I think it’s self explanatory, type is the radio button value used to determine which of the 2 attributes is required.

The thing is in the ajax validation response I don’t see anything in the json when 1 of the 2 fields is filled, but yii.js (or whatever other script) still refuses to submit the form.

If I browse the code using the developer tools (inspector) I can see the hidden div with the error:

<div class="invalid-feedback">Name cannot be blank.</div>

Even though I have the radio button with value of 1 (company) selected and the Company field is filled. In this case, name should not be required, but for some reason it still is.

Both name and company fields remain hidden until the user selects one of the 2 options. Which, if I’m not mistaken, both fields should remain optional because $model->type should be null therefore both anonymous functions return false.

Can you see where the problem is? Thanks.

After some more testing I can see there are problems with both the client-side and server-side scripts.

First Ajax validation request:

SignupForm[type]             ""
SignupForm[name]             ""
SignupForm[company]          ""
SignupForm[email]            "a@a.a"
SignupForm[password]         ""
SignupForm[confirm_password] ""
ajax                         "signupform"

Response:

{
    "signupform-type": ["Type cannot be blank."],
    "signupform-password": ["Password cannot be blank."],
    "signupform-confirm_password": ["Confirm Password cannot be blank."],
    "signupform-email": ["This email address has already been taken."]
}

As expected the server ajax response doesn’t require the optional fields.

I completely missed the last bit in conditional validation, the whenClient part. So I’ve implemented it but for some reason it is not working:

[['type'], 'required'],
[['type'], 'integer'],
[['type'], 'in', 'range' => [self::TYPE_PARTICULAR, self::TYPE_COMPANY]],

[['name'], 'required',
    'when' => function($model) { return $model->type === self::TYPE_PARTICULAR; },
    'whenClient' => "function (attribute, value) { return $( 'input[name=\"SignupForm[type]\"]' ).val() === " . self::TYPE_PARTICULAR . ' }'],
[['company'], 'required',
    'when' => function($model) { return $model->type === self::TYPE_COMPANY; },
    'whenClient' => "function (attribute, value) { return $( 'input[name=\"SignupForm[type]\"]' ).val() === " . self::TYPE_COMPANY . ' }'],
[['name', 'company'], 'string', 'max' => 50],

Now both fields are optional no matter what radio button I select.

Also, when I select a radio button option and trigger the Ajax validation:

SignupForm[type]             ""
SignupForm[type]             "1"
SignupForm[name]             ""
SignupForm[company]          ""
SignupForm[email]            "a@a.a"
SignupForm[password]         ""
SignupForm[confirm_password] ""
ajax                         "signupform"

Still the response doesn’t mention the signupform-company (which it should based on the model rules):

{
    "signupform-password": ["Password cannot be blank."],
    "signupform-confirm_password": ["Confirm Password cannot be blank."],
    "signupform-email": ["This email address has already been taken."]
}

I think this is a bug, should I open an issue?

Fixed, the problem was data type mismatch. JQuery returns a string and I was comparting to an integer, and the model validation does not convert the string with the type integer, I have to add an additional rule to perform the conversion.