On my experience in Yii IRC, once in a while there would be someone that asks about how to create pages that handles 1 main model and its submodel and saving the changes in a single click. i wrote the example application on top of Yii2's basic application template that you can download here (i suggest you also run the acceptance test to see it in action): Dynamic Tabular Form App
My example is a simple receipt page that allows a header and to add details dynamically. The tutorial also keeps the javascript dependency to a minimum.
Database ¶
- Create action features:
- Action create Steps:
- Update action features:
- ReceiptDetail model
- Update action steps:
- _form.php
[sql]
CREATE TABLE IF NOT EXISTS `receipt` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `receipt_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`receipt_id` int(11) DEFAULT NULL,
`item_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `receipt_detail_receipt_fk` (`receipt_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `receipt_detail`
ADD CONSTRAINT `receipt_detail_receipt_fk` FOREIGN KEY (`receipt_id`) REFERENCES `receipt` (`id`);
Create action features: ¶
- validate, create and link together the Receipt and ReceiptDetails
- have an addRow button that is used to add more rows in the page
- create scenario that will not require the receipt_id in the ReceiptDetail model, i call this SCENARIO_BATCH_UPDATE
all models here are new instances therefore there is not much complication on handling the page.
Action create Steps: ¶
- instantiate the Receipt model
- instantiate the $modelDetails array, this will hold the actual ReceiptDetail instances. The number of elements contained in this will mirror the number of rows seen in the page
- assign the $_POST['ReceiptDetail'] to $formDetails array
- iterate through $formDetails, create an instance of ReceiptDetail where you will massive assign each of the $formDetails
- check if the addRow button is clicked instead of the create button, if so... add a new ReceiptDetail instance to the $modelDetails and go to the create page
- if it's actually the create button that has been submitted, validate each $modelDetails and the $model which you have assigned the Receipt
- save!
/**
* Creates a new Receipt model.
* If creation is successful, the browser will be redirected to the 'view' page.
* @return mixed
*/
public function actionCreate()
{
$model = new Receipt();
$modelDetails = [];
$formDetails = Yii::$app->request->post('ReceiptDetail', []);
foreach ($formDetails as $i => $formDetail) {
$modelDetail = new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE]);
$modelDetail->setAttributes($formDetail);
$modelDetails[] = $modelDetail;
}
//handling if the addRow button has been pressed
if (Yii::$app->request->post('addRow') == 'true') {
$model->load(Yii::$app->request->post());
$modelDetails[] = new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE]);
return $this->render('create', [
'model' => $model,
'modelDetails' => $modelDetails
]);
}
if ($model->load(Yii::$app->request->post())) {
if (Model::validateMultiple($modelDetails) && $model->validate()) {
$model->save();
foreach($modelDetails as $modelDetail) {
$modelDetail->receipt_id = $model->id;
$modelDetail->save();
}
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('create', [
'model' => $model,
'modelDetails' => $modelDetails
]);
}
Update action features: ¶
this will be the more complicated part, these are the additional features that it will have over the create:
- updating of the Receipt and of the existing ReceiptDetail models
- inserting new ReceiptDetail model
- deletion of old ReceiptDetail model
ReceiptDetail model ¶
to support the update part, we need to add particular scenarios to our model, here's my ReceiptDetail code
/**
* This is the model class for table "receipt_detail".
*
* @property integer $id
* @property integer $receipt_id
* @property string $item_name
*
* @property Receipt $receipt
*/
class ReceiptDetail extends \yii\db\ActiveRecord
{
/**
* these are flags that are used by the form to dictate how the loop will handle each item
*/
const UPDATE_TYPE_CREATE = 'create';
const UPDATE_TYPE_UPDATE = 'update';
const UPDATE_TYPE_DELETE = 'delete';
const SCENARIO_BATCH_UPDATE = 'batchUpdate';
private $_updateType;
public function getUpdateType()
{
if (empty($this->_updateType)) {
if ($this->isNewRecord) {
$this->_updateType = self::UPDATE_TYPE_CREATE;
} else {
$this->_updateType = self::UPDATE_TYPE_UPDATE;
}
}
return $this->_updateType;
}
public function setUpdateType($value)
{
$this->_updateType = $value;
}
/**
* @inheritdoc
*/
public static function tableName()
{
return 'receipt_detail';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
['updateType', 'required', 'on' => self::SCENARIO_BATCH_UPDATE],
['updateType',
'in',
'range' => [self::UPDATE_TYPE_CREATE, self::UPDATE_TYPE_UPDATE, self::UPDATE_TYPE_DELETE],
'on' => self::SCENARIO_BATCH_UPDATE
],
['item_name', 'required'],
//allowing it to be empty because it will be filled by the ReceiptController
['receipt_id', 'required', 'except' => self::SCENARIO_BATCH_UPDATE],
[['receipt_id'], 'integer'],
[['item_name'], 'string', 'max' => 255]
];
}
Notice that i have an updateType virtual attribute, this is going to be used by us to determine what kind of operation are we going to do on a particular form row
This is what most people are looking for, have a streamlined page that the insertion, deletion, updating of the ReceiptDetail be done in 1 submission.
Update action steps: ¶
- loading of the main $model
- loading of the receiptDetail related models to the $modelDetails
- assign the $_POST['ReceiptDetail'] to $formDetails array
a. iterate through $formDetails, check if the the row includes an "ID", and if the 'updateType' is not calling for creation if this is the case, that means that it is asking us to load an existing ReceiptDetail model (regardless if it's for deletion or updating) b. if it's for creation, just like in our create Action we assign the $formDetail to a new instance of a ReceiptDetail
- check if the addRow button is clicked, if so then add a new instance of ReceiptDetail to $modelDetails and go to the update page afterwards
- if it's the "Save" button, validate all models. if the $modelDetail has an updateType for DELETE then call $modelDetail->delete() otherwise call save() (for UPDATE or CREATE)
- Done!
/**
* Updates an existing Receipt model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id
* @return mixed
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
$modelDetails = $model->receiptDetails;
$formDetails = Yii::$app->request->post('ReceiptDetail', []);
foreach ($formDetails as $i => $formDetail) {
//loading the models if they are not new
if (isset($formDetail['id']) && isset($formDetail['updateType']) && $formDetail['updateType'] != ReceiptDetail::UPDATE_TYPE_CREATE) {
//making sure that it is actually a child of the main model
$modelDetail = ReceiptDetail::findOne(['id' => $formDetail['id'], 'receipt_id' => $model->id]);
$modelDetail->setScenario(ReceiptDetail::SCENARIO_BATCH_UPDATE);
$modelDetail->setAttributes($formDetail);
$modelDetails[$i] = $modelDetail;
//validate here if the modelDetail loaded is valid, and if it can be updated or deleted
} else {
$modelDetail = new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE]);
$modelDetail->setAttributes($formDetail);
$modelDetails[] = $modelDetail;
}
}
//handling if the addRow button has been pressed
if (Yii::$app->request->post('addRow') == 'true') {
$modelDetails[] = new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE]);
return $this->render('update', [
'model' => $model,
'modelDetails' => $modelDetails
]);
}
if ($model->load(Yii::$app->request->post())) {
if (Model::validateMultiple($modelDetails) && $model->validate()) {
$model->save();
foreach($modelDetails as $modelDetail) {
//details that has been flagged for deletion will be deleted
if ($modelDetail->updateType == ReceiptDetail::UPDATE_TYPE_DELETE) {
$modelDetail->delete();
} else {
//new or updated records go here
$modelDetail->receipt_id = $model->id;
$modelDetail->save();
}
}
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('update', [
'model' => $model,
'modelDetails' => $modelDetails
]);
}
_form.php ¶
Form responsibilities:
- function as a regular form for the Receipt model
- for each instance of $modelDetail, display a row... with delete buttons
- have hidden inputs that store id and updateType, for create the id is NULL and the updateType is CREATE, for update the id is populated with updateType == UPDATE
- handle the delete button, if the row represents a ReceiptDetail has a scenario of update (this means it has been loaded from the DB) then we just mark it for deletion by making the updateType to DELETE otherwise if this is a new row that you just added using the addRow button, just remove the row entirely from the HTML page
use app\models\Receipt;
use app\models\ReceiptDetail;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model app\models\Receipt */
/* @var $form yii\widgets\ActiveForm */
/* @var $modelDetail app\models\ReceiptDetail */
?>
<?php $this->registerJs("
$('.delete-button').click(function() {
var detail = $(this).closest('.receipt-detail');
var updateType = detail.find('.update-type');
if (updateType.val() === " . json_encode(ReceiptDetail::UPDATE_TYPE_UPDATE) . ") {
//marking the row for deletion
updateType.val(" . json_encode(ReceiptDetail::UPDATE_TYPE_DELETE) . ");
detail.hide();
} else {
//if the row is a new row, delete the row
detail.remove();
}
});
");
?>
<div class="receipt-form">
<?php $form = ActiveForm::begin([
'enableClientValidation' => false
]); ?>
<?= $form->field($model, 'title')->textInput(['maxlength' => 255]) ?>
<?= "<h2>Details</h2>"?>
<?php foreach ($modelDetails as $i => $modelDetail) : ?>
<div class="row receipt-detail receipt-detail-<?= $i ?>">
<div class="col-md-10">
<?= Html::activeHiddenInput($modelDetail, "[$i]id") ?>
<?= Html::activeHiddenInput($modelDetail, "[$i]updateType", ['class' => 'update-type']) ?>
<?= $form->field($modelDetail, "[$i]item_name") ?>
</div>
<div class="col-md-2">
<?= Html::button('x', ['class' => 'delete-button btn btn-danger', 'data-target' => "receipt-detail-$i"]) ?>
</div>
</div>
<?php endforeach; ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
<?= Html::submitButton('Add row', ['name' => 'addRow', 'value' => 'true', 'class' => 'btn btn-info']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
adding initial rows to the create action
/** * Creates a new Receipt model. * If creation is successful, the browser will be redirected to the 'view' page. * @return mixed */ public function actionCreate() { $model = new Receipt(); $modelDetails = [new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE])]; $formDetails = Yii::$app->request->post('ReceiptDetail', []); foreach ($formDetails as $i => $formDetail) { if (isset($modelDetails[$i])) { $modelDetail = $modelDetails[$i]; $modelDetail->setAttributes($formDetail); } else { $modelDetail = new ReceiptDetail(['scenario' => ReceiptDetail::SCENARIO_BATCH_UPDATE]); $modelDetail->setAttributes($formDetail); $modelDetails[] = $modelDetail; } }
when trying to update
when trying to update it says
PHP Notice – yii\base\ErrorException Undefined variable: modelDetails 1. in /var/www/html/advanced/frontend/views/receipt/_form.php at line 98 8990919293949596979899100101102103104105106107 <div class="col-sm-2"> <?= $form->field($model, 'date_moneyorder')->widget(Datepicker::classname(), ['inline' => false,'clientOptions' => ['autoclose' => true,'format' => 'yyyy-m-d'],]);?> </div> </div> <?= "<h2>Details</h2>"?> <?php foreach ($modelDetails as $i => $modelDetail) : ?> <div class="row receipt-detail receipt-detail-<?= $i ?>"> <div class="col-md-11"> <?= Html::activeHiddenInput($modelDetail, "[$i]id") ?> <?= Html::activeHiddenInput($modelDetail, "[$i]updateType", ['class' => 'update-type']) ?> <div class="col-sm-6"> <?= $form->field($modelDetail, "[$i]item_name")->dropDownlist(ArrayHelper::map(TableParticulars::find()->all(), 'id', 'concatenated'), ['prompt' => 'Select a Particular ...']) ?> </div> <div class="col-sm-2"> <?= $form->field($modelDetail, "[$i]item_amount")->textInput(['maxlength' => true]) ?> 2. in /var/www/html/advanced/frontend/views/receipt/_form.php – yii\base\ErrorHandler::handleError(8, 'Undefined variable: modelDetails', '/var/www/html/advanced/frontend/...', 98, ...) at line 98 9293949596979899100101102103104 </div> <?= "<h2>Details</h2>"?> <?php foreach ($modelDetails as $i => $modelDetail) : ?> <div class="row receipt-detail receipt-detail-<?= $i ?>"> <div class="col-md-11"> <?= Html::activeHiddenInput($modelDetail, "[$i]id") ?> <?= Html::activeHiddenInput($modelDetail, "[$i]updateType", ['class' => 'update-type']) ?> <div class="col-sm-6"> <?= $form->field($modelDetail, "[$i]item_name")->dropDownlist(ArrayHelper::map(TableParticulars::find()->all(), 'id', 'concatenated'), ['prompt' => 'Select a Particular ...']) ?> 3. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – require('/var/www/html/advanced/frontend/...') at line 325 4. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – yii\base\View::renderPhpFile('/var/www/html/advanced/frontend/...', ['model' => frontend\models\Receipt]) at line 247 5. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – yii\base\View::renderFile('/var/www/html/advanced/frontend/...', ['model' => frontend\models\Receipt], null) at line 149 6. in /var/www/html/advanced/frontend/views/receipt/update.php – yii\base\View::render('_form', ['model' => frontend\models\Receipt]) at line 19 131415161718192021 <div class="receipt-update"> <h1><?= Html::encode($this->title) ?></h1> <?= $this->render('_form', [ 'model' => $model, ]) ?> </div> 7. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – require('/var/www/html/advanced/frontend/...') at line 325 8. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – yii\base\View::renderPhpFile('/var/www/html/advanced/frontend/...', ['model' => frontend\models\Receipt, 'modelDetails' => [frontend\models\ReceiptDetail]]) at line 247 9. in /var/www/html/advanced/vendor/yiisoft/yii2/base/View.php – yii\base\View::renderFile('/var/www/html/advanced/frontend/...', ['model' => frontend\models\Receipt, 'modelDetails' => [frontend\models\ReceiptDetail]], frontend\controllers\ReceiptController) at line 149 10. in /var/www/html/advanced/vendor/yiisoft/yii2/base/Controller.php – yii\base\View::render('update', ['model' => frontend\models\Receipt, 'modelDetails' => [frontend\models\ReceiptDetail]], frontend\controllers\ReceiptController) at line 371 11. in /var/www/html/advanced/frontend/controllers/ReceiptController.php – yii\base\Controller::render('update', ['model' => frontend\models\Receipt, 'modelDetails' => [frontend\models\ReceiptDetail]]) at line 167 161162163164165166167168169170171172173 } return $this->render('update', [ 'model' => $model, 'modelDetails' => $modelDetails ]); } /** * Deletes an existing Receipt model. * If deletion is successful, the browser will be redirected to the 'index' page. 12. frontend\controllers\ReceiptController::actionUpdate('17') 13. in /var/www/html/advanced/vendor/yiisoft/yii2/base/InlineAction.php – call_user_func_array([frontend\controllers\ReceiptController, 'actionUpdate'], ['17']) at line 55 14. in /var/www/html/advanced/vendor/yiisoft/yii2/base/Controller.php – yii\base\InlineAction::runWithParams(['r' => 'receipt/update', 'id' => '17']) at line 151 15. in /var/www/html/advanced/vendor/yiisoft/yii2/base/Module.php – yii\base\Controller::runAction('update', ['r' => 'receipt/update', 'id' => '17']) at line 455 16. in /var/www/html/advanced/vendor/yiisoft/yii2/web/Application.php – yii\base\Module::runAction('receipt/update', ['r' => 'receipt/update', 'id' => '17']) at line 84 17. in /var/www/html/advanced/vendor/yiisoft/yii2/base/Application.php – yii\web\Application::handleRequest(yii\web\Request) at line 375 18. in /var/www/html/advanced/frontend/web/index.php – yii\base\Application::run() ...
oh i see the error
i just add this code to _update.php
'modelDetails' => $modelDetails
thanks
Many to Many Relation with Junction table
I hope that you add another article deals with many to many relations with junction table. So
receipt_details
should be a junction table betweenreceipts
anditems
in which we may regardQty
of the item as an extra fields.Many to Many Relation with Junction table
i would say the tutorial is going to be the same except the receipt details are gonna include elements with dropdown boxes referencing the item table
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.