ActiveForm enableAjaxValidation issue

I encountered a strange bug yesterday. I have a form where I enabled the enableAjaxValidation and give a specific validation url to validate a coupon code. The problem append when I blurred or submitted the form. If the field was empty, it was redirecting to the validation url instead of making an ajax request. If the field was filled, the validation worked perfectly.

Even weirder, while searching for the solution, I found that if I added a validation rule in the Model just before the one about the field causing problem, it fixes the issue. Whatever the validation or the attribute… Seems like a time bomb to me so I think to script the AJAX manually for this form.

My guess is there’s a sequence of validation rules that creates an exception in the generation of the active form javascript.

Here’s the code (very simple) :

_form.php




    <?php $formCoupon = ActiveForm::begin([

        'id' => 'transaction-coupon-form',

        'enableAjaxValidation' => true,

        'validationUrl' => ['transaction', 'action' => TransactionAction::ACTION_VALIDATE_COUPON],

        'fieldConfig' => [

            'template' => "{input}{hint}{error}",

        ],

    ]); ?>

    <div class="row">

        <div class="col-lg-6">

            <?= $formCoupon->field($model, 'promoCode')->textInput(['maxlength' => true, 'placeholder' => 'Code Promo', 'disabled' => $validForm]) ?>

        </div>

        <div class="col-lg-4">

            <?=SimpleLink::widget([

                'type' => 'button',

                'content_tag' => 'submitInput',

                'label' => 'Appliquer',

            ])?>

        </div>

    </div>


    <?php ActiveForm::end(); ?>



The validation action




    public function validateCoupon()

    {

        $model = $this->controller->prepareTransaction();

        $request = \Yii::$app->getRequest();


        \Yii::$app->response->format = Response::FORMAT_JSON;

        

        if ($request->isPost && $model->load($request->post())) {

            return ActiveForm::validate($model, ['promoCode']);

        }

        

        return [];

    }



Model validation rules




    public function rules()

    {

        return array_merge([

            [['public_user_id', 'coupon_id', 'status', 'is_deleted', 'is_free', 'created_by_admin', 'source_transaction_id'], 'integer'],

            [['discount', 'subtotal', 'tps', 'tvq', 'total', 'order_number', 'secret_key', 'date_created', 'date_modified', 'billing_address', 'billing_first_name', 'billing_last_name', 'billing_email', 'billing_city', 'billing_postal_code'], 'required'],

            [['discount', 'subtotal', 'tps', 'tvq', 'total'], 'number'],

            [['date_created', 'date_modified', 'paid_date', 'recurring_end_date'], 'safe'],

            [['order_number'], 'string', 'max' => 12],

            [['billing_address'], 'string', 'max' => 100],

            [['comments'], 'string', 'max' => 1000],

            [['payment_method'], 'string', 'max' => 50],

            [['promoCode'], 'string', 'max' => 20],

            [['whateverAttribute'], 'string', 'max' => 20], // NEVER REMOVE - FIXES THE VALIDATION ISSUE

            [['promoCode'], 'validateCoupon'],

            [['billing_email'], 'email'],

            [['secret_key', 'billing_address', 'billing_first_name', 'billing_last_name', 'billing_email', 'billing_city', 'billing_postal_code'], 'string', 'max' => 100],

            [['coupon_id'], 'exist', 'skipOnError' => true, 'targetClass' => Coupon::className(), 'targetAttribute' => ['coupon_id' => 'id']],

            [['public_user_id'], 'exist', 'skipOnError' => true, 'targetClass' => PublicUser::className(), 'targetAttribute' => ['public_user_id' => 'id']],

            [['employer_id'], 'exist', 'skipOnError' => true, 'targetClass' => Employer::className(), 'targetAttribute' => ['employer_id' => 'id']],

            [['created_by_admin'], 'exist', 'skipOnError' => true, 'targetClass' => AdminUser::className(), 'targetAttribute' => ['created_by_admin' => 'id']],

            [['source_transaction_id'], 'exist', 'skipOnError' => true, 'targetClass' => Transaction::className(), 'targetAttribute' => ['source_transaction_id' => 'id']],

            //[['acceptTerms'], 'required', 'requiredValue' => 1, 'message' => 'Vous devez accepter les conditions générales de ventes.'],

        ], $this->rules);

    }



Validation code for the rule validateCoupon




    public function validateCoupon($attribute, $params, $validator)

    {

        $coupons = Coupon::find()->andWhere(['code' => $this->$attribute])

            ->andWhere(['<=', 'start_date', date('Y-m-d')])

            ->andWhere(['>=', 'end_date', date('Y-m-d')])

            ->all();


        if(!$coupons) {

            Yii::$app->transaction->removeCoupon();

            $this->addError($attribute, 'Le coupon n\'est pas valide.');

        } else {

            foreach($coupons as $coupon)

            {

                if($coupon->count && $coupon->getNumberOfUses() >= $coupon->count)

                {

                    $this->addError($attribute, 'Le coupon n\'est plus disponible.');

                    continue;

                }


                // Add the coupon

                Yii::$app->transaction->addCoupon($coupon->id);

                break;

            }

        }

    }



why don’t you add required rule for your promoCode field so it will check for empty input, and make sure you put it above your string rule like so.




[['promoCode'], 'required'],

[['promoCode'], 'string', 'max' => 20]

Thanks for your response. In fact, the promo code isn’t required at all so it won’t resolve my issue. I have took the time to recode the Ajax for the validation of this field based on the yii.validation.js script to make sure I had the same classes. I also had a bad mechanism for my coupons so I fixed both of my problems at the same time.

Here’s my javascript code if anybody need some example in the future.




    $('#btn-validate-coupon').on('click', function(e){


        var couponData = {'Transaction[promoCode]':$('#transaction-promocode').val()};

        var $container = $('#transaction-promocode').closest(".form-group");       

        var $container = $container.addClass("validating");       


        var classes = {

            // the container CSS class representing the corresponding attribute has validation error

            errorCssClass: 'has-error',

            // the container CSS class representing the corresponding attribute passes validation

            successCssClass: 'has-success',

            // the container CSS class representing the corresponding attribute is being validated

            validatingCssClass: 'validating',

        };


        // Reset field

        $container.removeClass(

            classes.validatingCssClass + ' ' +

            classes.errorCssClass + ' ' +

            classes.successCssClass

        );


        $container.find('.help-block-error').text('');


        $.ajax({

            url: $(this).attr('data-validation'),

            type: 'get',

            dataType: 'json',

            data: couponData

        }).done(function(data){

            data = data === 0 ? [] : data;


            if('transaction-promocode' in data)

            {

                $container.addClass(classes.errorCssClass);

                $container.find('.help-block-error').text(data['transaction-promocode']);

            } else {

                $container.addClass(classes.successCssClass);

            }


            $.transaction('getTotal');

        });


        e.stopImmediatePropagation();

        e.preventDefault();

        return false;

    });



Just for people to know about my bad mecanism in case some are wondering, I had a payment form based on my Transaction model in which the "promoCode" attribute is located. In the same view I had 2 forms : one for my billing infos, and one for my coupon code. My cart is stored into the session and is encrypted with all the products, their options and the coupon. And I build the Transaction model based from the information stored into my session to validate the integrity of it upon payment.

My logic was to seperate those so I could use the ajax validation on the coupon code only (being stored in the session after validation). I was a bad logic since the coupon HAD to be validated first to be applied to my transaction.