Skip to content

Nested Forms Example

shangchen1127 edited this page Feb 27, 2020 · 6 revisions

Preface

Many thanks to the DoingITeasyChannel on YouTube which helped me get started on using this widget with nested forms.

I have the following code working on my Yii2 Site, and I'm very thank for this widget. @wbraganca you've done a great job with this. I hope I can give back by posting some helpful code here.

I recommend reading ALL of the example before copying the code.

Example Image

Note that the screen shot is cut off along the top. There are 3 more fields above those that are shown. Example of Nested Forms

FORM code

Starting with the View here, you can see I have three parts to my form. There is a header from the model Deposit, a parent DynamicForm from the model Payment, and a child/nested DynamicForm from the model PaymentLoads.

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;
use wbraganca\dynamicform\DynamicFormWidget;

?>

<div class="receipt-deposit-form">

<!-- The Deposit Information    -->
    <?php $form = ActiveForm::begin(['id' => 'deposit-form']); ?>
    <div class="row">
        <div class="col-sm-4">
            <?= $form->field($modelDeposit, 'effective_date')->textInput(['maxlength' => true]) ?>
        </div>
        <div class="col-sm-4">
            <?= $form->field($modelDeposit, 'staff_id')->textInput(['maxlength' => true]) ?>
        </div>
        <div class="col-sm-4">
            <?= $form->field($modelDeposit, 'billing_currency_id')->textInput(['maxlength' => true]) ?>
        </div>
    </div>

<!-- The Receipts on the Deposit -->
    <div class="row panel-body">
        <?php DynamicFormWidget::begin([
            'widgetContainer' => 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_]
            'widgetBody' => '.container-payments', // required: css class selector
            'widgetItem' => '.payment-item', // required: css class
            'insertButton' => '.add-payment', // css class
            'deleteButton' => '.del-payment', // css class
            'model' => $modelsPayment[0],
            'formId' => 'deposit-form',
            'formFields' => [
                'created_date',
                'cash_receipt_id',
                'voided',
            ],
        ]); ?>


        <h4>Deposit Receipts</h4>
        <table class="table table-bordered">
            <thead>
                <tr class="active">
                    <td></td>
                    <td><?= Html::activeLabel($modelsPayment[0], 'created_date'); ?></td>
                    <td><?= Html::activeLabel($modelsPayment[0], 'cash_receipt_id'); ?></td>
                    <td><?= Html::activeLabel($modelsPayment[0], 'voided'); ?></td>
                    <td><label class="control-label">Receipt Items</label></td>
                </tr>
            </thead>

            <tbody class="container-payments"><!-- widgetContainer -->
            <?php foreach ($modelsPayment as $i => $modelPayment): ?>
                <tr class="payment-item"><!-- widgetBody -->
                    <td>
                        <button type="button" class="del-payment btn btn-danger btn-xs"><i class="glyphicon glyphicon-minus"></i></button>
                        <?php
                        // necessary for update action.
                        if (! $modelPayment->isNewRecord) {
                            echo Html::activeHiddenInput($modelPayment, "[{$i}]id");
                        }
                        ?>
                    </td>
                    <td>
                        <?php
                            echo $form->field($modelPayment, "[{$i}]created_date")->begin();
                            echo Html::activeTextInput($modelPayment, "[{$i}]created_date", ['maxlength' => true, 'class' => 'form-control']); //Field
                            echo Html::error($modelPayment,"[{$i}]created_date", ['class' => 'help-block']); //error
                            echo $form->field($modelPayment, "[{$i}]created_date")->end();
                        ?>
                    </td>
                    <td>
                        <?php
                            echo $form->field($modelPayment, "[{$i}]cash_receipt_id")->begin();
                            echo Html::activeTextInput($modelPayment, "[{$i}]cash_receipt_id", ['maxlength' => true, 'class' => 'form-control']); //Field
                            echo Html::error($modelPayment,"[{$i}]cash_receipt_id", ['class' => 'help-block']); //error
                            echo $form->field($modelPayment, "[{$i}]cash_receipt_id")->end();
                        ?>
                    </td>
                    <td>
                        <?php
                            if(!$modelPayment->isNewRecord && $modelPayment->cashReceipt->voided) {
                                $modelPayment->voided = $modelPayment->cashReceipt->voided;
                            }
                            echo $form->field($modelPayment, "[{$i}]voided")->begin();
                            echo Html::activeCheckbox($modelPayment, "[{$i}]voided", ['class' => 'form-control', 'label'=>'']); //Field
                            echo Html::error($modelPayment,"[{$i}]voided", ['class' => 'help-block']); //error
                            echo $form->field($modelPayment, "[{$i}]voided")->end();
                        ?>
                    </td>

<!-- The Items on the Receipt -->
                    <td id="payment_loads">

                        <?php DynamicFormWidget::begin([
                            'widgetContainer' => 'dynamicform_inner', // required: only alphanumeric characters plus "_" [A-Za-z0-9_]
                            'widgetBody' => '.container-loads', // required: css class selector
                            'widgetItem' => '.load-item', // required: css class
                            'insertButton' => '.add-load', // css class
                            'deleteButton' => '.del-load', // css class
                            'model' => $modelsPaymentLoads[$i][0],
                            'formId' => 'deposit-form',
                            'formFields' => [
                                'departure_nav_waypt_airport_id',
                                'destination_nav_waypt_airport_id',
                                'load_rate_id',
                                'price',
                            ],
                        ]);

                        ?>

                        <table class="table table-bordered">
                            <thead>
                                <tr class="active">
                                    <td></td>
                                    <td><?= Html::activeLabel($modelsPaymentLoads[$i][0], 'departure_nav_waypt_airport_id'); ?></td>
                                    <td><?= Html::activeLabel($modelsPaymentLoads[$i][0], 'destination_nav_waypt_airport_id'); ?></td>
                                    <td><?= Html::activeLabel($modelsPaymentLoads[$i][0], 'load_rate_id'); ?></td>
                                    <td><?= Html::activeLabel($modelsPaymentLoads[$i][0], 'paymentLoadsPrice.ttl_price_target'); ?></td>
                                </tr>
                            </thead>
                            <tbody class="container-loads"><!-- widgetContainer -->
                            <?php foreach ($modelsPaymentLoads[$i] as $ix => $modelPaymentLoads): ?>
                                <tr class="load-item"><!-- widgetBody -->
                                    <td>
                                        <button type="button" class="del-load btn btn-danger btn-xs"><i class="glyphicon glyphicon-minus"></i></button>
                                        <?php
                                        // necessary for update action.
                                        if (! $modelPaymentLoads->isNewRecord) {
                                            echo Html::activeHiddenInput($modelPaymentLoads, "[{$i}][{$ix}]id");
                                        }
                                        ?>
                                    </td>

                                    <td>
                                        <?php
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]departure_nav_waypt_airport_id")->begin();
                                            echo Html::activeTextInput($modelPaymentLoads, "[{$i}][{$ix}]departure_nav_waypt_airport_id", ['maxlength' => true, 'class' => 'form-control']); //Field
                                            echo Html::error($modelPaymentLoads,"[{$i}][{$ix}]departure_nav_waypt_airport_id", ['class' => 'help-block']); //error
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]departure_nav_waypt_airport_id")->end();
                                        ?>
                                    </td>
                                    <td>
                                        <?php
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]destination_nav_waypt_airport_id")->begin();
                                            echo Html::activeTextInput($modelPaymentLoads, "[{$i}][{$ix}]destination_nav_waypt_airport_id", ['maxlength' => true, 'class' => 'form-control']); //Field
                                            echo Html::error($modelPaymentLoads,"[{$i}][{$ix}]destination_nav_waypt_airport_id", ['class' => 'help-block']); //error
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]destination_nav_waypt_airport_id")->end();
                                        ?>

                                    </td>
                                    <td>
                                        <?php
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]load_rate_id")->begin();
                                            echo Html::activeTextInput($modelPaymentLoads, "[{$i}][{$ix}]load_rate_id", ['maxlength' => true, 'class' => 'form-control']); //Field
                                            echo Html::error($modelPaymentLoads,"[{$i}][{$ix}]load_rate_id", ['class' => 'help-block']); //error
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]load_rate_id")->end();
                                        ?>
                                    </td>
                                    <td>
                                        <?php

                                            if(!$modelPaymentLoads->isNewRecord) {
                                                $modelPaymentLoads->price = $modelPaymentLoads->readLoadPrice();
                                            }
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]price")->begin();
                                            echo Html::activeTextInput($modelPaymentLoads, "[{$i}][{$ix}]price", ['maxlength' => true, 'class' => 'form-control']); //Field
                                            echo Html::error($modelPaymentLoads,"[{$i}][{$ix}]price", ['class' => 'help-block']); //error
                                            echo $form->field($modelPaymentLoads, "[{$i}][{$ix}]price")->end();
                                        ?>
                                    </td>

                                </tr>
                            <?php endforeach; // end of loads loop ?>
                            </tbody>
                            <tfoot>
                                <td colspan="5" class="active"><button type="button" class="add-load btn btn-success btn-xs"><i class="glyphicon glyphicon-plus"></i></button></td>
                            </tfoot>
                        </table>
                        <?php DynamicFormWidget::end(); // end of loads widget ?>

                    </td> <!-- loads sub column -->
                </tr><!-- payment -->
            <?php endforeach; // end of payment loop ?>
            </tbody>
            <tfoot>
                <td colspan="5" class="active">
                    <button type="button" class="add-payment btn btn-success btn-xs"><i class="glyphicon glyphicon-plus"></i></button>
                </td>
            </tfoot>
        </table>
        <?php DynamicFormWidget::end(); // end of payment widget ?>

    </div>
    
    <div class="form-group">
        <?= Html::submitButton($modelPayment->isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-primary']) ?>
    </div>
                

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

Modification of the Original Model Class

In order to make this work, the original Model Class had to be modified. The following two changes were made:

  • Added $data = null to the function arguments.
  • Changed the initialization of the $post variable to be $post = empty($data) ? Yii::$app->request->post($formName) : $data[$formName];

Here is the complete Model Class which I renamed to DynamicForms.

<?php

/* 
 * This model was copied from the following site:
 * https://github.com/wbraganca/yii2-dynamicform
 */

namespace common\models;

use Yii;
use yii\helpers\ArrayHelper;

class DynamicForms extends \yii\base\Model
{
    /**
     * Creates and populates a set of models.
     *
     * @param string $modelClass
     * @param array $multipleModels
     * @param array $data
     * @return array
     */
    public static function createMultiple($modelClass, $multipleModels = [], $data = null)
    {
        $model    = new $modelClass;
        $formName = $model->formName();
        // added $data=null to function arguments
        // modified the following line to accept new argument
        $post     = empty($data) ? Yii::$app->request->post($formName) : $data[$formName];
        $models   = [];

        if (! empty($multipleModels)) {
            $keys = array_keys(ArrayHelper::map($multipleModels, 'id', 'id'));
            $multipleModels = array_combine($keys, $multipleModels);
        }

        if ($post && is_array($post)) {
            foreach ($post as $i => $item) {
                if (isset($item['id']) && !empty($item['id']) && isset($multipleModels[$item['id']])) {
                    $models[$i] = $multipleModels[$item['id']];
                } else {
                    $models[$i] = new $modelClass;
                }
            }
        }

        unset($model, $formName, $post);

        return $models;
    }
}

CONTROLLER code

To reuse code between the Create and Update functions, I created an additional function called saveDeposit. The Create and Update actions call this function to save the data from the DynamicForm.

public function actionDepositCreate() {

    $modelDeposit = new ReceiptDeposits();
    $modelsPayment = [new Payment()];
    $modelsPaymentLoads[] = [new PaymentLoads];

    if ($modelDeposit->load(Yii::$app->request->post())) {

        // get Payment data from POST
        $modelsPayment = DynamicForms::createMultiple(Payment::classname());
        DynamicForms::loadMultiple($modelsPayment, Yii::$app->request->post());

        // get PaymentLoads data from POST
        $loadsData['_csrf'] =  Yii::$app->request->post()['_csrf'];
        for ($i=0; $i<count($modelsPayment); $i++) {
            $loadsData['PaymentLoads'] =  Yii::$app->request->post()['PaymentLoads'][$i];
            $modelsPaymentLoads[$i] = DynamicForms::createMultiple(PaymentLoads::classname(),[] ,$loadsData);
            DynamicForms::loadMultiple($modelsPaymentLoads[$i], $loadsData);
        }

        // validate all models - see example online for ajax validation
        // https://github.com/wbraganca/yii2-dynamicform
        $valid = $modelDeposit->validate();
        $valid = Payment::validateDeposit($modelsPayment,$modelsPaymentLoads) && $valid;

        // save deposit data
        if ($valid) {
            if ($this->saveDeposit($modelDeposit,$modelsPayment,$modelsPaymentLoads)) {
                Yii::$app->getSession()->setFlash('success',
                    Yii::t('payments','The deposit number {id} has been saved.', ['id' => $modelDeposit->id]));
                return $this->redirect('deposit-index-all');
            }
        }
    }

    return $this->render('depositCreate', [
        'titles' => $this->createTitlesDeposits(),
        'modelDeposit' => $modelDeposit,
        'modelsPayment'  => (empty($modelsPayment)) ? [new Payment] : $modelsPayment,
        'modelsPaymentLoads' => (empty($modelsPaymentLoads)) ? [new PaymentLoads] : $modelsPaymentLoads,
    ]);
}



public function actionDepositUpdate($id) {

    // retrieve existing Deposit data
    $modelDeposit = $this->findModelDeposit($id);

    // retrieve existing Payment data
    $oldPaymentIds = Payment::find()->select('id')
        ->where(['receipt_deposits_id' => $id])->asArray()->all();
    $oldPaymentIds = ArrayHelper::getColumn($oldPaymentIds,'id');
    $modelsPayment = Payment::findAll(['id' => $oldPaymentIds]);
    $modelsPayment = (empty($modelsPayment)) ? [new Payment] : $modelsPayment;

    // retrieve existing Loads data
    $oldLoadIds = [];
    foreach ($modelsPayment as $i => $modelPayment) {
        $oldLoads = PaymentLoads::findAll(['payment_id' => $modelPayment->id]);
        $modelsPaymentLoads[$i] = $oldLoads;
        $oldLoadIds = array_merge($oldLoadIds,ArrayHelper::getColumn($oldLoads,'id'));
        $modelsPaymentLoads[$i] = (empty($modelsPaymentLoads[$i])) ? [new PaymentLoads] : $modelsPaymentLoads[$i];
    }

    // handle POST
    if ($modelDeposit->load(Yii::$app->request->post())) {

        // get Payment data from POST
        $modelsPayment = DynamicForms::createMultiple(Payment::classname(), $modelsPayment);
        DynamicForms::loadMultiple($modelsPayment, Yii::$app->request->post());
        $newPaymentIds = ArrayHelper::getColumn($modelsPayment,'id');

        // get PaymentLoads data from POST
        $newLoadIds = [];
        $loadsData['_csrf'] =  Yii::$app->request->post()['_csrf'];
        for ($i=0; $i<count($modelsPayment); $i++) {
            $loadsData['PaymentLoads'] =  Yii::$app->request->post()['PaymentLoads'][$i];
            $modelsPaymentLoads[$i] = DynamicForms::createMultiple(PaymentLoads::classname(),$modelsPaymentLoads[$i] ,$loadsData);
            DynamicForms::loadMultiple($modelsPaymentLoads[$i], $loadsData);
            $newLoadIds = array_merge($newLoadIds,ArrayHelper::getColumn($loadsData['PaymentLoads'],'id'));
        }

        // delete removed data
        $delLoadIds = array_diff($oldLoadIds,$newLoadIds);
        if (! empty($delLoadIds)) PaymentLoads::deleteAll(['id' => $delLoadIds]);
        $delPaymentIds = array_diff($oldPaymentIds,$newPaymentIds);
        if (! empty($delPaymentIds)) Payment::deleteAll(['id' => $delPaymentIds]);

        // validate all models
        $valid = $modelDeposit->validate();
        $valid = Payment::validateDeposit($modelsPayment,$modelsPaymentLoads) && $valid;

        // save deposit data
        if ($valid) {
            if ($this->saveDeposit($modelDeposit,$modelsPayment,$modelsPaymentLoads)) {
                Yii::$app->getSession()->setFlash('success',
                    Yii::t('payments','The deposit number {id} has been saved.', ['id' => $modelDeposit->id]));
                return $this->redirect('deposit-index-all');
            }
        }
    }

    // show VIEW
    return $this->render('depositUpdate', [
        'titles' => $this->createTitlesDeposits(),
        'modelDeposit' => $modelDeposit,
        'modelsPayment'  => $modelsPayment,
        'modelsPaymentLoads' => $modelsPaymentLoads,
    ]);
}


/**
 * This function saves each part of the deposit dynamic form controls.
 *
 * @param $modelDeposit mixed The Deposit model.
 * @param $modelsPayment mixed The Payment model from the deposit.
 * @param $modelsPaymentLoads mixed The PaymentLoads model from the deposit.
 * @return bool Returns TRUE if successful.
 * @throws NotFoundHttpException When record cannot be saved.
 */
protected function saveDeposit($modelDeposit,$modelsPayment,$modelsPaymentLoads ) {
    $transaction = Yii::$app->fs->beginTransaction();
    try {
        if ($go = $modelDeposit->save(false)) {

            // loop through each payment
            foreach ($modelsPayment as $i => $modelPayment) {
                // save the payment record
                $modelPayment->cash_payment = true;
                $modelPayment->cash_billing_currency_id = $modelDeposit->billing_currency_id;
                if ($go = $modelPayment->save(false) and !$modelPayment->voided) {
                    // loop through each load
                    foreach ($modelsPaymentLoads[$i] as $ix => $modelPaymentLoads) {
                        // save the load record
                        $modelPaymentLoads->payment_id = $modelPayment->id;
                        if (! ($go = $modelPaymentLoads->save(false))) {
                            $transaction->rollBack();
                            break;
                        }

                    }
                }
            }
        }
        if ($go) {
            $transaction->commit();
        }
    } catch (Exception $e) {
        $transaction->rollBack();
    }
    return $go;
}

depositCreate VIEW code

I've included the Create file here for those interested in how I styled the panel.

<?php

use common\widgets\DataTitles;
use yii\helpers\Html;
use yii\web\View;

/* @var $this View */
/* @var $titles DataTitles */
/* @var $model app\models\fs\Account */

$this->title = $titles->windowCreate();

?>
<div class="receipt-deposit-create">

    <div class="panel panel-success">
        <div class="panel-heading">
            <h3 class="panel-title"><?= $titles->panelCreate() ?></h3>
        </div>
    
        <div class="panel-body">
            <?= $this->render('_deposit', [
                'modelDeposit' => $modelDeposit,
                'modelsPayment'  => $modelsPayment,
                'modelsPaymentLoads' => $modelsPaymentLoads,
            ]) ?>
        </div>
    </div>
    

</div>