-
Notifications
You must be signed in to change notification settings - Fork 441
Nested Forms Example
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.
Note that the screen shot is cut off along the top. There are 3 more fields above those that are shown.
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>
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;
}
}
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;
}
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>