Tabular input with dynamically added instances and clientSideValidation

Dear Yii2-framework community.

First of all, I would like to express my appreciation for the work done.

However, since the first version of the framework, I still could not find a fully conventional, “bulletproof” method for solving the problem described below. There were an enormous amount of topics, both here and on SO, but I haven’t found the one, which had an ideal answer.

I would be very happy, if someone could finally help me to solve it, because I face this problem almost in every project.

We have two related models: ContactInfo and ExtraPhoneNumber




    public function getExtraPhoneNumbers()

    {

        return $this->hasMany(ExtraPhoneNumber::className(), ['contact_info_id' => 'id']);

    }



The problem is to save parent model ContactInfo with multiple instances of dynamically added child model ExtraPhoneNumber.

What is done (please, feel free to correct me if my approach of adding the row dynamically could be better)

VIEW _form.php:




<div class="contact-info-form">


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

    ]); ?>


    <?= $form->field($model, 'title')->textInput(['maxlength' => true]) ?>


    <?= $form->field($model, 'primary_phone')->textInput() ?>


    <?php $phoneIndexCounter = 0; foreach($extraPhoneNumbers as $index=>$extraPhoneNumber): ?>

        <?php echo $this->render('_extra-phone', [

            'model' => $extraPhoneNumber,

            'index' => $index,

        ]); ?>

    <?php $phoneIndexCounter++; endforeach; ?>


    <?php echo Html::a('<i class="glyphicon glyphicon-plus"></i> Add phone', '#', ['class'=>'add_phone btn btn-success']); ?>


    <div class="form-group">

        <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>

    </div>


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


</div>


<?php $this->registerJs('

$(function(){

    var phoneIndexCounter = '.$phoneIndexCounter.';

    $(".add_phone").click(function(e){

        e.preventDefault();

        $.ajax({

            url: "/admin/contact-info/add-extra-phone",

            type: "get",

            data: {

                index: phoneIndexCounter++,

            },

            success: function(data){

                $(".add_phone").before(data);

            },

            error: function(){

                alert(Error);

            },

        });

    });

});

');


?>



PARTIAL VIEW _extra-phone.php




<div class="form-group">

<?php 

    echo Html::activeLabel($model,"[$index]phone");

    echo Html::activeTextInput($model,"[$index]phone", ['class' => 'form-control half-width']);

    echo Html::error($model,"[$index]phone");

?>

<?php echo Html::a('Delete row', '#', ['class'=>'del_row btn btn-danger', 'onclick' => 'deleteChild(this); return false;']); ?>


</div>


<?php $this->registerJs('

function deleteChild(elm)

{

    element=$(elm).parent();

    $(element).remove();

};

', \yii\web\View::POS_END); ?>



CONTROLLER ACTION FOR ADDING THE ROW:




public function actionAddExtraPhone($index)

    {

        $model = new ExtraPhoneNumber();

        echo $this->renderPartial('/contact-info/_extra-phone', array(

            'model' => $model,

            'index' => $index,

        ), false, true);

    }



AND THE MOST DIFFICULT PART - an update action, which doesn’t work:




public function actionUpdate($id)

    {

        $request = Yii::$app->request->post();


        $model = $this->findModel($id);

        $extraPhoneNumbers = $model->extraPhoneNumbers;


        if($model->load($request) && $model->save()) 

        {

            if (Model::loadMultiple($extraPhoneNumbers, $request) && Model::validateMultiple($extraPhoneNumbers)) {

                foreach ($extraPhoneNumbers as $extraPhoneNumber) {

                    $extraPhoneNumber->contact_info_id = $model->id;

                    $extraPhoneNumber->save(false);

                }

            } else {

                return $this->render('update', [

                    'model' => $model,

                    'extraPhoneNumbers' => $extraPhoneNumbers,

                ]);

            }


            return $this->redirect(['view', 'id' => $model->id]);


        } else {

            return $this->render('update', [

                'model' => $model,

                'extraPhoneNumbers' => $extraPhoneNumbers,

            ]);

        }

    }



When I first met the “loadMultiple” function, I’ve decided that this is the Holy Grail and all my dreams come true, but it doesn’t collect the post data for dynamically added model instances. Besides that, my cliendSideValidation doesn’t work for related model.

Furthemore, if parent model has some validation errors, than the data, that was added dynamically, won’t be processed at all and you have to refill it again.

I think, it could be done like:




if($model->load($request) && Model::loadMultiple($extraPhoneNumbers, $request) && $model->save()) 



but it looks not logical at all and doesn’t solve the main issue.

Please, help me finally understand the situation, so that I could never return and bother you with this issue.

Thank you in advance.

For the meanwhile, I’ll put my workaround here. I believe it is too ugly and becomes totally messy, if we need to add another related model. And what is worse - enableCliendValidation still doesn’t work here.




public function actionUpdate($id)

    {

        $request = Yii::$app->request->post();


        $model = $this->findModel($id);

        $extraPhoneNumbers = $model->extraPhoneNumbers;


        if($model->load($request) && $model->save()) 

        {

            if(count($extraPhoneNumbers) > 0) {

                foreach ($extraPhoneNumbers as $k => $v) {

                    $v->delete();

                    unset($extraPhoneNumbers[$k]);

                }

            }


            if(Yii::$app->getRequest()->post('ExtraPhoneNumber')){

                foreach(Yii::$app->getRequest()->post('ExtraPhoneNumber') as $k=>$v){

                    if(!isset($extraPhoneNumbers[$k])){

                        $extraPhoneNumbers[$k] = new ExtraPhoneNumber;

                    }

                }

            }


            if (Model::loadMultiple($extraPhoneNumbers, Yii::$app->getRequest()->post(), 'ExtraPhoneNumber') && Model::validateMultiple($extraPhoneNumbers)) {

                foreach ($extraPhoneNumbers as $single_phone) {

                        $model->link('extraPhoneNumbers', $single_phone);

                }

            }


            return $this->redirect(['view', 'id' => $model->id]);


        } else {

            return $this->render('update', [

                'model' => $model,

                'extraPhoneNumbers' => $extraPhoneNumbers,

            ]);

        }

    }



Anyone for help here? ::)

It’s not too messy and in fact, I would make it more so, not delete all the phone records each time, but compare the two arrays and decide whether to insert/update or delete.

However, I think a more elegant solution would be to code the whole form in Ajax and then you can update each part on the fly.

Dear Chris,

Thanks for reply.

I do agree that deleting all phone records is not a good decision. The problem is that if any of added rows won’t pass validation - all records are already deleted.

Don’t you have any working example of how could be dynamically added rows validated on the client side?

Thank you