Dynamic Tabular Input Form

Hey guys, I want to create a dynamic multiple model form. Basically, it will begin with one block of a model and contain a "Add Another" button which will add another model to the form dynamically.

Something like this:


<?php $form = ActiveForm::begin(); ?>


    <div id="block-0">

    <?= $form->field($model, '[0]field1')->textInput(['maxlength' => 45]) ?>

    

    <?= $form->field($model, '[0]field2')->textInput(['maxlength' => 45]) ?>

    

    <?= $form->field($model, '[0]field3')->textInput(['maxlength' => 45]) ?>

    </div>


    <div id="additional"></div>


    <?= Html::button("Add Another", ['class' => 'btn btn-primary'], 'id' => 'button-add-another'); ?>

    

    <div class="form-group">

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

    </div>


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

I want the "Add Another" button to append another block ("block-1" would be the next one) to the "additional" div with the same model fields. I tried one way to do it with no luck.

For the record, I know how to collect tabular input in the controller, validate it, and save it. The only problems I have is the logic in the view for dynamically creating (duplicating) multiple model fields.

I believe that dynamic tabular input forms are quite frequent (that’s the case in my projects at least). Do you have any suggestions?

Have. You already read this: http://www.yiiframework.com/wiki/362/how-to-use-multiple-instances-of-the-same-model-in-the-same-form/

You may want to try my yii2-builder extension which has a tabular form builder. Similar to use like a grid view (you could check out 2 demo scenarios).

Adding a new row needs to be done through a normal controller actionCreate (with or without ajax) - for example popping up a modal dialog… and then refreshing the grid.

Hey, I just read this, and unfortunately, is not what I’m looking for. Again, I know how to handle the controller logic and add multiple model instances statically, but I am looking for creating model instances dynamically.

For example, let’s say I have an address form. The form starts with only one instance of the model, and after the “Add Another” button is clicked a new model is created (a new set of fields bellow the originals). There will also be a “Remove” button for each additional set of fields.

I’ve tried to do something but it was an extremely bad approach. Specifically, every time the “Add Another” button is clicked, I serialized the form through AJAX, created an array of models based on the form data and just added another empty model to the array. Then, I updated the <div> for the form with the newly created array of models. So I am creating a new form on each “Add Another” click. I’ve had problems with client validation for this though. I’ll try to post the code tomorrow and maybe we can find a better solution together.

Hey man, this is not what I am looking for currently, but it’s very good s*** ;D. It will come extremely handy in future projects. Well done!

P.S. I saw your other extensions. Really good stuff. Great job ;)

Ok, here is how I solved this (a bad solution though).

In this example I use an Address model. Here is the structure of the files:

models/Address.php

controllers/AddressController.php

views/address/create-batch.php

views/address/_form-batch.php

views/address/_form-batch/additional.php

[b]

Controller Logic (AddressController.php):[/b]


/**

     * Creates a new array of Address models for batch insert.

     * If creation is successful, the browser will be redirected to the 'index' page.

     * @return mixed

     */

    public function actionCreateBatch()

    {

        //loading one initial model to array

        $model = array();

        $model[] = new Address();

        

        //creating dynamic models from post

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

        if ($postData !== null && is_array($postData)) {

            $model = array();

            foreach ($postData as $i => $single) {

                $model[$i] = new Address();

            }

        }

        //end of creating dynamic models post

        

        //validating + saving models

        if(Address::loadMultiple($model, Yii::$app->request->post()) && Address::validateMultiple($model)) {

            foreach ($model as $item) {

                $item->save(false);

            }


            return $this->redirect(['index']);

        }

        //end of validation + saving models

        

        return $this->render('create-batch', [

            'model' => $model,

        ]);

    }

    

    /**

     * Adds an Additional model to the batch form

     * The post data received is from the AJAX call of the Add Another Button

     * @throws HttpException

     */

    public function actionAddAdditionalRow()

    {

        if(\Yii::$app->getRequest()->getIsAjax()) {

            //this is the post data from the ajax request

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

            if (empty($postData) || !is_array($postData)) {

                throw new HttpException(500, 'An internal server error has occured.');

            }

            

            $model = array();

            //creating existing model instances + setting attributes

            foreach ($postData as $i => $single) {

                $model[$i] = new Address();

                $model[$i]->setAttributes($single);

            }

            //end of creating existing model instances + setting attributes

            

            //creating an additional empty model instance

            $model[] = new Address();

            

            // it has to be renderAjax in order to include the script validation files

            echo $this->renderAjax('_form-batch/additional', array("model" => $model));

            exit;


        } else {

            throw new HttpException(404, 'Unable to resolve the request: address/add-additional-row');

        }

    }

create-batch.php:


<?php


use yii\helpers\Html;


/**

 * @var yii\web\View $this

 * @var app\models\Address $model

 */


$this->title = 'Create Address';

$this->params['breadcrumbs'][] = ['label' => 'Addresses', 'url' => ['index']];

$this->params['breadcrumbs'][] = $this->title;

?>

<div class="address-create">


    <h1><?= Html::encode($this->title) ?></h1>


    <?= $this->render('_form-batch', [

        'model' => $model,

    ]) ?>


</div>

[b]

_form-batch.php:[/b]


<?php


use yii\helpers\Html;

use yii\widgets\ActiveForm;


/**

 * @var yii\web\View $this

 * @var app\models\Address $model

 * @var yii\widgets\ActiveForm $form

 */

?>


<div id="div-address-form" class="address-form">


    <?php $form = ActiveForm::begin(); ?>


    <div class="row">

        <?php foreach ($model as $i => $single): ?>

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

                <?= $form->field($single, "[$i]city")->textInput(['maxlength' => 35]) ?>


                <?= $form->field($single, "[$i]address_line_one")->textInput(['maxlength' => 255]) ?>


                <?= $form->field($single, "[$i]adress_line_two")->textInput(['maxlength' => 255]) ?>


                <?= $form->field($single, "[$i]address_line_three")->textInput(['maxlength' => 255]) ?>


                <?= $form->field($single, "[$i]telephone")->textInput(['maxlength' => 20]) ?>

            </div>

        <?php endforeach; ?>

    </div>


    <div class="form-group">

        <button type="button" id="button-add-another" class="btn btn-primary">Add Another</button>

    </div>

    

    <div class="form-group">

        <?= Html::submitButton('Create', ['class' => 'btn btn-success']) ?>

    </div>


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


</div>

<?php

//The AJAX request for the Add Another button. It updates the #div-address-form div.

$this->registerJs("

    $(document).on('click', '#button-add-another', function(){

        $.ajax({

            url: '" . \Yii::$app->urlManager->createUrl(['address/add-additional-row']) . "',

            type: 'post',

            data: $('#" . $form->id . "').serialize(),

            dataType: 'html',

            success: function(data) {

                $('#div-address-form').html(data);

            },

            error: function() {

                alert('An error has occured while adding a new block.');

            }

        });

    });

"); ?>

_form-batch/additional.php:


<?php


use yii\helpers\Html;

use yii\widgets\ActiveForm;


/**

 * Updates the #div-address-form through AJAX. (called from address/add-additional-row)

 * @var yii\web\View $this

 * @var app\models\Address $model

 * @var yii\widgets\ActiveForm $form

 */

?>

<!-- Pay attention to the action URL otherwise it would default to address/add-additional-row -->

<?php $form = ActiveForm::begin(['action' => \Yii::$app->urlManager->createUrl(['address/create-batch'])]); ?>


<div class="row">

    <?php foreach ($model as $i => $single): ?>

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

            <?= $form->field($single, "[$i]city")->textInput(['maxlength' => 35]) ?>


            <?= $form->field($single, "[$i]address_line_one")->textInput(['maxlength' => 255]) ?>


            <?= $form->field($single, "[$i]adress_line_two")->textInput(['maxlength' => 255]) ?>


            <?= $form->field($single, "[$i]address_line_three")->textInput(['maxlength' => 255]) ?>


            <?= $form->field($single, "[$i]telephone")->textInput(['maxlength' => 20]) ?>

        </div>

    <?php endforeach; ?>

</div>


<div class="form-group">

    <button type="button" id="button-add-another" class="btn btn-primary">Add Another</button>

</div>


<div class="form-group">

    <?= Html::submitButton('Create', ['class' => 'btn btn-success']) ?>

</div>


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

Overall, this is how it works.

1.) index.php?r=address/create-batch is called (AddressController).

2.) In actionCreateBatch(), an array of Address models is created, with only one model added to the array in the beginning.

3.) _form-batch.php is rendered with the Address models cycled in foreach.

4.) The Add Another button calls actionAddAdditionalRow() through AJAX and sends the serialized form through post.

5.) In actionAddAdditionalRow(), a new array of Address Models is created based on the serialized form and the attributes are set.

6.) An additional empty Address model is added to the array.

7.) actionAddAdditionalRow() renders additional.php, therefore replacing the #div-address-form div with the new form.

8.) Check the screenshots.

Now, even though this works, here are some reasons why this is a bad solution.

1.) The code repeats. The form code in _form-batch.php and additional.php is the same.

2.) Scripts are added after the AJAX call and therefore they overlap with the initial scripts added before the </body>

3.) If renderPartial is used instead of renderAjax and the scripts are not included, client-side validation doesn’t work for the additional blocks.

4.) Every time the "Add Additional" button is clicked, the validation messages disappear.

So help me out guys, I know I have overcomplicated my code but it was the only solution I could come up with. Can you improve it?

Hey markot

Did you find any solution for the same, as i have also ran into the same issue. Do you have suggestion how to improve this further.

ok, i have finally implemented this code.

now the problem is that in stead of appending a new ajax request to the actual fomrm. you send the actual Data with POST to the server, there you generate a new form and load it in place of the old.

to avoid, this the ajax request should only load a part of the form and append it to the actual form.

i will try to continue to improve this code.

also i want to add form fields without new lables as for example:

Label phone — [__555 - 1256 _] [== Delete ==]

__________ — [_________] [== Delete ==]

[== Add new phone ==]

Hey guys,

Sorry I was away I’ve been busy these last months and didn’t get a chance to play with Yii 2. I’ll try to take a look at the code again these days.

aBott, could you please provide us with a code sample if you have already found a solution?

I need some assistance on this as well. Does anyone have a clean solution for this yet?

i am also looking for a solution, does anyone has a solution for this ?

I’ll actually be working something similar to this in the coming days/weeks. If I figure anything out, I’ll post here.

here is an extension that is doing what you guys are searching for :rolleyes:

I already implement it, but not working while using dropdownlist,

the dropdownlist can’t load the data

yeah… same probleme here, trying to implement select2 or typeahead … but nothing works( it works only on first tab

… on second clone not working, you found any solutions ?

how to add row and how to make ADD button active… Please tell me solution… I am very new in Yii…

waiting for reply… thnx…

Has anyone solved this yet?

I’m currently just dynamically adding


Html::activeTextInput($mod,"[$i]address");

since I can’t figure out how to add to the existing activeform…

Hey guys, have a look at this. I’ve made a little progress, but still haven’t been able to solve the heart of the problem.

http://www.yiiframework.com/forum/index.php/topic/59871-how-to-enable-validation-for-dynamically-created-field/page__view__findpost__p__270723

I’ve also been struggling with what “should” be quite a simple and commonplace task.

I want one place where my shared form snippets live so that I can use them separately when assembling a form server side OR as snippets I can retrieve and insert into an existing form via ajax.

The problem with the normal Yii2 approach is that I need to create a form to render my form fields. However, you cannot (afaik) create a form without rendering it. I was truly surprised to see an echo statement in yii2\widgets\ActiveForm::init but there it is, if you create a form you MUST echo it. Even fiddling around with output buffering doesn’t play well with Yii2.

I would argue that this is an architectural boo boo in the framework. Even calling a render method in a web controller returns a string, no magic echo statements, you either return it or echo it.

My pretty horrible workaround is a 2 part cludge:

  1. create a controller action to return a form only containing the partial you want eg



Controller

public function actionTemplate()

{

	return $this->renderAjax('template', [

		'model' => new myModel(),

		'form' => new ActiveForm(['layout' => 'horizontal']),

		'index' => Yii::$app->request->get('index', 1)

	]);

}




Index in the above is useful if you are keeping count of how many new models you have created on the client side.

  1. Here’s the nasty part 2 in the client side code




$('body').on('click', '#addContactNumber', function(){

	contactNumberTemplate();

})


var contactNumberTemplate = function()

	{

$.get(url, data, function(response){

	var content = $(response).find('.divClass');// turn the HTML response into a jquery object and select the part you want to insert in your form, ignoring the rest, especially the form it is wrapped in

	$('#placeholder').append(content);

	contactNumberIndex++;

})

}




My form snippet looks like




<?php

use common\models\CustomerContactNumber;

?>

<div class="divClass">

<?php

echo $form->field($contactNumber, "[$index]id")->hiddenInput()->label(false);

echo $form->field($contactNumber, "[$index]customer_id")->hiddenInput()->label(false);

echo $form->field($contactNumber, "[$index]number")->textInput();

echo $form->field($contactNumber, "[$index]type")->dropDownList(CustomerContactNumber::typeLabels());?>

</div>




I don’t like this at all but I also haven’t found any elegant way of doing this in forums/stackoverflow etc. I hope this gets addressed soon or someone enlightens me:)

Hey Guys,

I’m also struggling with this issue and try to find a elegant solution.

One might think this is a "common" task with tons of available solutions & easy to implement.

But the more you read the more you get the feeling that this is something like the "Yii2 Holy grail".

So what we can say for sure in summary to make things clear for others who are having the problem?

I would really appreciate a good tutorial / article about the best way to let clients ADD / REMOVE form inputs dynamically WITHOUT breaking any validation for the form elements.

Is there already a final solution that works with complex forms?

I’m currently looking at all solutions posted here and somewhere else…

But all seem not very "elegant" and look like a lot of work when you have multiple forms which need this feature.

I will now try out / take a look at "yii2-dynamicform" extension and keep you updated.

Best Regards