Dynamic Rows During View/update

I have a model EmployeeEducation which describes the educational background of a specific employee. I want to display all retrieved information through a table view. If I use findByAttributes it returns the latest entry, but findAllByAttributes returns an array. I don’t know if I’m doing it right by using foreach to separate data, also the part to submit the form:


<?php echo CHtml::submitButton($educ[0]->isNewRecord ? 'Create' : 'Save'); ?>

Will $_POST["EmployeeEducation"] contain all $educ or $educ[0] only?

This is what it currently looks like:

Below is my view _education.php.


<?php

$educ=EmployeeEducation::model()->findAllByAttributes(array('employee_id'=>$model->employee_id));

$form=$this->beginWidget('CActiveForm', array(

    'enableClientValidation'=>true,

    'clientOptions'=>array('validateOnSubmit'=>true),

));

?>


    <p class="note">Fields with <span class="required">*</span> are required.</p>

    

    <p>III. Educational Background</p>

    <table>

    <tr>

        <th><?php echo $form->labelEx($educ[0],'level'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'school_name'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'degree_course'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'year_graduated'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'highest_grade'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'from_year'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'to_year'); ?></th>

        <th><?php echo $form->labelEx($educ[0],'honors_received'); ?></th>

    </tr>

    

    <?php

    foreach ($educ as $val)

        echo '<tr>

        <td>'. $form->dropDownList($val,'level',EmployeeEducation::model()->level) .'</td>

        <td>'. $form->textField($val,'school_name') .'</td>

        <td>'. $form->textField($val,'degree_course') .'</td>

        <td>'. $form->textField($val,'year_graduated') .'</td>

        <td>'. $form->textField($val,'highest_grade') .'</td>

        <td>'. $form->textField($val,'from_year') .'</td>

        <td>'. $form->textField($val,'to_year') .'</td>

        <td>'. $form->textField($val,'honors_received') .'</td>

        </tr>';

    ?>

    </table>

    

    <div class="row buttons">

        <?php echo CHtml::submitButton($educ[0]->isNewRecord ? 'Create' : 'Save'); ?>

        <?php echo CHtml::resetButton('Reset'); ?>

    </div>


<?php $this->endWidget(); ?>

I also need to place an [ADD] button to dynamically create a new row to store additional educational information (ie. for multiple graduate studies).

To start with the first question: you won’t receive array of fields in your POST data - probably it will be first or last row of your form. All you have to do is change form fields creation into:


   

$i = 0;

foreach ($educ as $val)

        echo '<tr>

        <td>'. $form->dropDownList($val,'level',EmployeeEducation::model()->level, array('name' => 'EmployeeEducation['.$i.'][level]')) .'</td>

        <td>'. $form->textField($val,'school_name', array('name' => 'EmployeeEducation['.$i.'][school_name]')) .'</td>

        <td>'. $form->textField($val,'degree_course', array('name' => 'EmployeeEducation['.$i.'][degree_course]')) .'</td>

        <td>'. $form->textField($val,'year_graduated', array('name' => 'EmployeeEducation['.$i.'][year_graduated]')) .'</td>

        <td>'. $form->textField($val,'highest_grade', array('name' => 'EmployeeEducation['.$i.'][highest_grade]')) .'</td>

        <td>'. $form->textField($val,'from_year', array('name' => 'EmployeeEducation['.$i.'][from_year]')) .'</td>

        <td>'. $form->textField($val,'to_year', array('name' => 'EmployeeEducation['.$i.'][to_year]')) .'</td>

        <td>'. $form->textField($val,'honors_received', , array('name' => 'EmployeeEducation['.$i++.'][honors_received]')) .'</td>

        </tr>';

Then you can iterate in back-end code in a following way




foreach($_POST["EmployeeEducation"] as $row)

 save row 



When it comes to button for dynamically created row you have to way of doing it:

  1. Using mostly PHP: you have button that will call ajax action that generates html and then injects those html code (that comes as response) at the end of the table that stores all rows.

  2. Using pure javascript: you can copy existing row (or create new if in your case you can have zero rows). If copied then you’ll need to change index in the attribute ‘name’ of all fields in the copied row.

In 1st solution you have to perform following steps:

create partial view file called eg. _form_row in the same directory you have view _education.php that contain following php code:




        echo '<tr class="form-row">

        <td>'. CHtml::activeDropDownList($val,'level',EmployeeEducation::model()->level, array('name' => 'EmployeeEducation['.$i.'][level]')) .'</td>

        <td>'. CHtml::activeTextField($val,'school_name', array('name' => 'EmployeeEducation['.$i.'][school_name]')) .'</td>

        <td>'. CHtml::activeTextField($val,'degree_course', array('name' => 'EmployeeEducation['.$i.'][degree_course]')) .'</td>

        <td>'. CHtml::activeTextField($val,'year_graduated', array('name' => 'EmployeeEducation['.$i.'][year_graduated]')) .'</td>

        <td>'. CHtml::activeTextField($val,'highest_grade', array('name' => 'EmployeeEducation['.$i.'][highest_grade]')) .'</td>

        <td>'. CHtml::activeTextField($val,'from_year', array('name' => 'EmployeeEducation['.$i.'][from_year]')) .'</td>

        <td>'. CHtml::activeTextField($val,'to_year', array('name' => 'EmployeeEducation['.$i.'][to_year]')) .'</td>

        <td>'. CHtml::activeTextField($val,'honors_received', , array('name' => 'EmployeeEducation['.$i++.'][honors_received]')) .'</td>

        </tr>';



then you can change your existing view _education.php. into




    <?php

    $i = 0;

    foreach ($educ as $val)

        $this->renderPartial('_form_row', array('val'=>$val, 'i' => $i++));

    ?>



Next you need to create ajax action that will render only one render with given index.




    public function actionAjaxAddRow($index)

    {

        $this->renderPartial('_form_row', array('val'=>new EmployeeEducation, 'i' => $index));

        Yii::app()->end();

    }



dont forget to add this action to array returned by accessRules method

Last step is to add the button itself into your main view view _education.php. Add following code somewhere in you view:




echo CHtml::button('Add row', array('id' => 'add-row'));



and at the beginning of your view file add javascript handling clicking on the button




<script>

$('#add-row').click(function(){

	$.ajax({

		url: "<? php echo $this->createUrl('ajaxAddRow') ?>",

		data: {

			index: $('.form-row').size()

		},

		dataType: 'html',

		type: 'GET',

		success: function(html){

			$('#form-table').append(html);

		}

	});

});

</script>



also change modify this line in your view




<p>III. Educational Background</p>

    <table id='form-table'>



This is first solution of this problem and personally I wouldn’t do it this way. It requires ajax call for thing than can be done using only javascript. If you want I can write you second solution if you are interested.

Thank you for this information! But should I still use $educ[0], $educ, or $val for the submit?


<?php echo CHtml::submitButton($educ[0]->isNewRecord ? 'Create' : 'Save'); ?>

Also, let’s say I have the image above and I dynamically added a new row. How should I declare the model for $educ->save()?


$educ = EmployeeEducation::model()->findByAttributes(array('employee_id'=>$id));

or

$educ = new EmployeeEducation;

The first one updates the existing data but won’t create new ones right?

The second one creates new data only.

By the way, is it possible to use CActiveForm instead of CHtml? I plan on having ajax on submit/client validation for the forms. But it seems impossible to pass $form from _education.php to _form_row.php, right?

yes you are right

and again yes right

‘CHtml::activeTextFiled’ is equivalent to ‘$form->textFiled’ and it will handle ajax validation the same way as ‘$form’ does so you can safely use CHtml but you have to change @mrk code to make validation work.

Add ‘true’ option to renderPartial parameters like this:


renderPartial('_form_row', array('val'=>$val, 'i' => $i++), false, TRUE);

I can now properly render rows and update rows. But there’s no validation of forms. The page just refreshes when I enter invalid/incomplete data.

This is the controller:




public function actionUpdate()

{

    $flag = true;

    if(isset($_POST['EmployeeEducation']))

    {

        foreach($_POST['EmployeeEducation'] as $row)

        {

            $educ = EmployeeEducation::model()->findByAttributes(array('education_id'=>$row['education_id']));

            $educ->attributes = $row;

            if(!$educ->save())

            {

                $flag = false;

                break;

            }

        }

        if ($flag) $this->redirect(array('view'));

    }

    $this->renderPartial('_education',array(

        'model'=>$model,

    ),false,true);

}

The view:




$educ = EmployeeEducation::model()->findAllByAttributes(array(

    'employee_id'=>$model->employee_id),

    array('order'=>'from_year'));

    $i = 0;

    foreach ($educ as $row)

        $this->renderPartial('_forms',array('form'=>$form,

            'row'=>$row,'i'=>$i++),false,true);



The dynamic rows:




echo '<tr class="form-row">

<td>'. $form->hiddenField($row,'education_id',array('name' => 'EmployeeEducation['.$i.'][education_id]'))

     . $form->dropDownList($row,'level',EmployeeEducation::model()->level,array('name' => 'EmployeeEducation['.$i.'][level]')) .'</td>

<td>'. $form->textField($row,'school_name',array('name' => 'EmployeeEducation['.$i.'][school_name]')) .'</td>

<td>'. $form->textField($row,'degree_course',array('name' => 'EmployeeEducation['.$i.'][degree_course]')) .'</td>

<td>'. $form->textField($row,'year_graduated',array('name' => 'EmployeeEducation['.$i.'][year_graduated]')) .'</td>

<td>'. $form->textField($row,'highest_grade',array('name' => 'EmployeeEducation['.$i.'][highest_grade]')) .'</td>

<td>'. $form->textField($row,'from_year',array('name' => 'EmployeeEducation['.$i.'][from_year]')) .'</td>

<td>'. $form->textField($row,'to_year',array('name' => 'EmployeeEducation['.$i.'][to_year]')) .'</td>

<td>'. $form->textField($row,'honors_received',array('name' => 'EmployeeEducation['.$i.'][honors_received]')) .'</td>

</tr>';



faridplus’ suggestion to add renderPartial parameters makes the form render alone. I forgot to mention my _education form is inside a CJuiTabs tab.

What happens when you call $model->save() is that when this saving fails then internal error array of model is filled with error messages.

Problem in your case is because of the fact that models array in your view is retrieved directly in your view file - even if a while ago the same models were validated in your controller.

Just change your controller code into:


public function actionUpdate()

{

    $flag = true;

    if(isset($_POST['EmployeeEducation']))

    {

        $models = array();

        foreach($_POST['EmployeeEducation'] as $row)

        {

            $educ = EmployeeEducation::model()->findByAttributes(array('education_id'=>$row['education_id']));

            $educ->attributes = $row;

            $models[] = $educ;

            if(!$educ->save())

            {

                $flag = false;

                break; // you can consider removing it if want to have all rows

//validated not only 1st invalid

            }

        }

        if ($flag) $this->redirect(array('view'));

    }

    else{

       $models = EmployeeEducation::model()->findAllByAttributes(array(

                      'employee_id'=>$model->employee_id), // here you use model variable which has to be 

//instantiated somewhere above but in your code it just appears from nowhere

                       array('order'=>'from_year'));

    } 

    $this->renderPartial('_education',array(

        'model'=>$model, 'models' => $models

    ),false,true);

}

and your view into:


    $i = 0;

    foreach ($models as $row)

        $this->renderPartial('_forms',array('form'=>$form,

            'row'=>$row,'i'=>$i++),false,true);

Now your should have your fields validated.

Besides this code


$educ = EmployeeEducation::model()->findByAttributes(array('education_id'=>$row['education_id']));

won’t work for newly created fields.

change it to:


$educ = empty($row['education_id']) ? new EmployeeEducation : // you may also need feeling this model with soe initial foreign keys like employee_id 

EmployeeEducation::model()->findByAttributes(array('education_id'=>$row['education_id']));

I found this post is very useful to me,I 've try to implement invoice system but with no success yet,I would like to know how to implement this by using js only like @mrk said.Thx.

Purely JS solution is also very easy.

Assume we have following structure for our form




<div id='rows'>

<div class='row'>

  <select name="ModelName[0][relation_id]">

    <option>some option</option>

    ...

  </select>

  <field name="ModelName[0][attr1]" value="">

  <field name="ModelName[0][attr2]" value="">

</div>


</div>



of course it can be much richer with labels etc, but let’s omit it for clarity. Let’s assume this one div with “row” class is always in view - view can generate it easily either there are some relations in DB or not. What you want to do is dynamically add more rows basing on this one existing. All you have to do is copy the whole div with class “row”, then set proper indices inside name attributes of form elements. Let’s code it:




 var pattern = $('#rows.row').first().clone();


        var prepareRow = function(row, rowCount)

        {

            row.find('select').attr('name', 'ModelName[' + rowCount + '][relation_id]');

            row.find('input[type=text]').attr('name', 'ModelName[' + rowCount + '][attr1]');

            row.find('input[type=text]').attr('name', 'ModelName[' + rowCount + '][attr2]');

            row.find('option').removeAttr('selected');

            row.find('input[type=text]').attr('value', '');

            

            return row;

        }


        var function addRowButtonHandler = function()

        {

            var row = prepareRow(pattern.clone(), $('#rows').children('.row').size());

        

            $('#rows').append(row); 

        }    




You just need to assign addRowButtonHandler to onclick event for some button and then you have to adjust this piece of code for your form - name of relation_id field, number of text fields and name of attributes in model those fields are responsible for.

I hope you find it usable.