Yii2: Handling Tabular Data and Dynamic Form Field

Saya sudah mencoba beberapa extension untuk menghandle tabular data dengan field yang dinamis. Namun saya terkesan dengan extension yii2-dynamicform buatan wbraganca silahkan cekidot di https://github.com/wbraganca/yii2-dynamicform, cukup lengkap baik fitur, dokumentasi dan demonya.

Saya akan merekonstruksi bagaimana step by step menggunakan extension ini serta beberapa nasehat yang mudah-mudahan bermanfaat bagi Anda. Tutorial ini hanya merekonstruksi sehingga demo dari hasil rekonstruksi ini bisa Anda lihat di official demonya http://wbraganca.com/yii2extensions/dynamicform-demo3, yap saya memilih demo yang paling kompleks yaitu Demo 3 – (Nested Dynamic Form), karena itu Anda juga bisa melihat source code disini http://wbraganca.com/yii2extensions/dynamicform-demo3/source-code

Kasus yang dicontohkan disana adalah membuat user inteface yang efisien untuk input 3 tabel yaitu Person, House dan Room. Dimana Person bisa memiliki lebih dari satu House, dan masing masing House bisa memiliki lebih dari satu Room.

Catatan: Pada tutorial ini saya menggunakan template basic, untuk template advance silahkan menyesuaikan

Baiklah, tak usah berpanjang kalam, langsung saja
1. Buatlah tabelnya sebagai berikut berikut:
Skema

tabel

SQL Source

CREATE TABLE `house` (
  `id` int(11) NOT NULL,
  `person_id` int(11) DEFAULT NULL,
  `description` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `person` (
  `id` int(11) NOT NULL,
  `first_name` varchar(50) NOT NULL,
  `last_name` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `room` (
  `id` int(11) NOT NULL,
  `house_id` int(11) DEFAULT NULL,
  `description` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `house`
  ADD PRIMARY KEY (`id`),
  ADD KEY `person_id` (`person_id`);

ALTER TABLE `person`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `room`
  ADD PRIMARY KEY (`id`),
  ADD KEY `house_id` (`house_id`);

ALTER TABLE `house`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;

ALTER TABLE `person`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;

ALTER TABLE `room`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;

ALTER TABLE `house`
  ADD CONSTRAINT `house_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE `room`
  ADD CONSTRAINT `room_ibfk_1` FOREIGN KEY (`house_id`) REFERENCES `house` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

Nasehat:
- Hendaknya constraint relasinya cascade on delete & update sehingga ketika tabel person dihapus maka semua akan terhapus.
- Foreign key (person_id dan house_id) hendaknya diset NULL supaya ketika validate tidak gagal..
- Pastikan field primary key menggunakan nama ‘id’ bukan ID atau yang lain

2. Generate Models untuk ketiga tabel tersebut menggunakan Gii (gak usah sok-sokan pake cara manual :))
3. Generate CRUD untuk Person saja, yap karena CRUD untuk kasus ini adanya hanya di tabel Person.
4. Tambhakan 1 model lagi, untuk handling form tabular yaitu app\models\Model.php

<?php

namespace app\models;

use Yii;
use yii\helpers\ArrayHelper;

class Model extends \yii\base\Model
{
    /**
     * Creates and populates a set of models.
     *
     * @param string $modelClass
     * @param array $multipleModels
     * @return array
     */
    public static function createMultiple($modelClass, $multipleModels = [])
    {
        $model    = new $modelClass;
        $formName = $model->formName();
        $post     = Yii::$app->request->post($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[] = $multipleModels[$item['id']];
                } else {
                    $models[] = new $modelClass;
                }
            }
        }

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

        return $models;
    }
}

5. Ubah PersonController pada actionCreate dan actionUpdate

public function actionCreate()
{
    $modelPerson = new Person;
    $modelsHouse = [new House];
    $modelsRoom = [[new Room]];

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

        $modelsHouse = Model::createMultiple(House::classname());
        Model::loadMultiple($modelsHouse, Yii::$app->request->post());

        // validate person and houses models
        $valid = $modelPerson->validate();
        $valid = Model::validateMultiple($modelsHouse) && $valid;

        if (isset($_POST['Room'][0][0])) {
            foreach ($_POST['Room'] as $indexHouse => $rooms) {
                foreach ($rooms as $indexRoom => $room) {
                    $data['Room'] = $room;
                    $modelRoom = new Room;
                    $modelRoom->load($data);
                    $modelsRoom[$indexHouse][$indexRoom] = $modelRoom;
                    $valid = $modelRoom->validate();
                    //die(var_dump($modelRoom->errors));
                }
            }
        }
        if ($valid) {
            $transaction = Yii::$app->db->beginTransaction();
            try {
                if ($flag = $modelPerson->save(false)) {
                    foreach ($modelsHouse as $indexHouse => $modelHouse) {

                        if ($flag === false) {
                            break;
                        }

                        $modelHouse->person_id = $modelPerson->id;

                        if (!($flag = $modelHouse->save(false))) {
                            break;
                        }

                        if (isset($modelsRoom[$indexHouse]) && is_array($modelsRoom[$indexHouse])) {
                            foreach ($modelsRoom[$indexHouse] as $indexRoom => $modelRoom) {
                                $modelRoom->house_id = $modelHouse->id;
                                if (!($flag = $modelRoom->save(false))) {
                                    break;
                                }
                            }
                        }
                    }
                }

                if ($flag) {
                    $transaction->commit();
                    return $this->redirect(['view', 'id' => $modelPerson->id]);
                } else {
                    $transaction->rollBack();
                }
            } catch (Exception $e) {
                $transaction->rollBack();
            }
        }
    }

    return $this->render('create', [
        'modelPerson' => $modelPerson,
        'modelsHouse' => (empty($modelsHouse)) ? [new House] : $modelsHouse,
        'modelsRoom' => (empty($modelsRoom)) ? [[new Room]] : $modelsRoom,
    ]);
}

dan

public function actionUpdate($id)
{
    $modelPerson = $this->findModel($id);
    $modelsHouse = $modelPerson->houses;
    $modelsRoom = [];
    $oldRooms = [];

    if (!empty($modelsHouse)) {
        foreach ($modelsHouse as $indexHouse => $modelHouse) {
            $rooms = $modelHouse->rooms;
            $modelsRoom[$indexHouse] = $rooms;
            $oldRooms = ArrayHelper::merge(ArrayHelper::index($rooms, 'id'), $oldRooms);
        }
    }

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

        // reset
        $modelsRoom = [];

        $oldHouseIDs = ArrayHelper::map($modelsHouse, 'id', 'id');
        $modelsHouse = Model::createMultiple(House::classname(), $modelsHouse);
        Model::loadMultiple($modelsHouse, Yii::$app->request->post());
        $deletedHouseIDs = array_diff($oldHouseIDs, array_filter(ArrayHelper::map($modelsHouse, 'id', 'id')));

        // validate person and houses models
        $valid = $modelPerson->validate();
        $valid = Model::validateMultiple($modelsHouse) && $valid;

        $roomsIDs = [];
        if (isset($_POST['Room'][0][0])) {
            foreach ($_POST['Room'] as $indexHouse => $rooms) {
                $roomsIDs = ArrayHelper::merge($roomsIDs, array_filter(ArrayHelper::getColumn($rooms, 'id')));
                foreach ($rooms as $indexRoom => $room) {
                    $data['Room'] = $room;
                    $modelRoom = (isset($room['id']) && isset($oldRooms[$room['id']])) ? $oldRooms[$room['id']] : new Room;
                    $modelRoom->load($data);
                    $modelsRoom[$indexHouse][$indexRoom] = $modelRoom;
                    $valid = $modelRoom->validate();
                }
            }
        }

        $oldRoomsIDs = ArrayHelper::getColumn($oldRooms, 'id');
        $deletedRoomsIDs = array_diff($oldRoomsIDs, $roomsIDs);

        if ($valid) {
            $transaction = Yii::$app->db->beginTransaction();
            try {
                if ($flag = $modelPerson->save(false)) {

                    if (! empty($deletedRoomsIDs)) {
                        Room::deleteAll(['id' => $deletedRoomsIDs]);
                    }

                    if (! empty($deletedHouseIDs)) {
                        House::deleteAll(['id' => $deletedHouseIDs]);
                    }

                    foreach ($modelsHouse as $indexHouse => $modelHouse) {

                        if ($flag === false) {
                            break;
                        }

                        $modelHouse->person_id = $modelPerson->id;

                        if (!($flag = $modelHouse->save(false))) {
                            break;
                        }

                        if (isset($modelsRoom[$indexHouse]) && is_array($modelsRoom[$indexHouse])) {
                            foreach ($modelsRoom[$indexHouse] as $indexRoom => $modelRoom) {
                                $modelRoom->house_id = $modelHouse->id;
                                if (!($flag = $modelRoom->save(false))) {
                                    break;
                                }
                            }
                        }
                    }
                }

                if ($flag) {
                    $transaction->commit();
                    return $this->redirect(['view', 'id' => $modelPerson->id]);
                } else {
                    $transaction->rollBack();
                }
            } catch (Exception $e) {
                $transaction->rollBack();
            }
        }
    }

    return $this->render('update', [
        'modelPerson' => $modelPerson,
        'modelsHouse' => (empty($modelsHouse)) ? [new House] : $modelsHouse,
        'modelsRoom' => (empty($modelsRoom)) ? [[new Room]] : $modelsRoom
    ]);
}

Catatan, ada 3 models yang digunakan dan satu helpers dan belum di -USE, silahkan di Use.. yaitu:

use app\models\House;
use app\models\Room;
use app\models\Model;
use yii\helpers\ArrayHelper;

6. Modifikasi beberapa views untuk menyesuaikan dengan controller diatas

a. app\views\person\Create.php
Edit bagian render seperti berikut

<?= $this->render('_form', [
  'modelPerson' => $modelPerson,
  'modelsHouse' => $modelsHouse,
  'modelsRoom' => $modelsRoom,
]) ?>

b. app\views\person\Update.php
Disini juga edit bagian render seperti berikut

<?= $this->render('_form', [
  'modelPerson' => $modelPerson,
  'modelsHouse' => $modelsHouse,
  'modelsRoom' => $modelsRoom,
]) ?>

c. app\views\person\_form.php

<?php

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

?>

<div class="person-form">

    <?php $form = ActiveForm::begin(['id' => 'dynamic-form']); ?>

     <div class="row">
        <div class="col-sm-6">
            <?= $form->field($modelPerson, 'first_name')->textInput(['maxlength' => true]) ?>
        </div>
        <div class="col-sm-6">
            <?= $form->field($modelPerson, 'last_name')->textInput(['maxlength' => true]) ?>
        </div>
    </div>

    <div class="padding-v-md">
        <div class="line line-dashed"></div>
    </div>

    <?php DynamicFormWidget::begin([
        'widgetContainer' => 'dynamicform_wrapper',
        'widgetBody' => '.container-items',
        'widgetItem' => '.house-item',
        'limit' => 10,
        'min' => 1,
        'insertButton' => '.add-house',
        'deleteButton' => '.remove-house',
        'model' => $modelsHouse[0],
        'formId' => 'dynamic-form',
        'formFields' => [
            'description',
        ],
    ]); ?>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th>Houses</th>
                <th style="width: 450px;">Rooms</th>
                <th class="text-center" style="width: 90px;">
                    <button type="button" class="add-house btn btn-success btn-xs"><span class="fa fa-plus"></span></button>
                </th>
            </tr>
        </thead>
        <tbody class="container-items">
        <?php foreach ($modelsHouse as $indexHouse => $modelHouse): ?>
            <tr class="house-item">
                <td class="vcenter">
                    <?php
                        // necessary for update action.
                        if (! $modelHouse->isNewRecord) {
                            echo Html::activeHiddenInput($modelHouse, "[{$indexHouse}]id");
                        }
                    ?>
                    <?= $form->field($modelHouse, "[{$indexHouse}]description")->label(false)->textInput(['maxlength' => true]) ?>
                </td>
                <td>
                    <?= $this->render('_form-rooms', [
                        'form' => $form,
                        'indexHouse' => $indexHouse,
                        'modelsRoom' => $modelsRoom[$indexHouse],
                    ]) ?>
                </td>
                <td class="text-center vcenter" style="width: 90px; verti">
                    <button type="button" class="remove-house btn btn-danger btn-xs"><span class="fa fa-minus"></span></button>
                </td>
            </tr>
         <?php endforeach; ?>
        </tbody>
    </table>
    <?php DynamicFormWidget::end(); ?>

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

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

</div>

d. app\views\person\_form-room.php

<?php

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

?>

<?php DynamicFormWidget::begin([
    'widgetContainer' => 'dynamicform_inner',
    'widgetBody' => '.container-rooms',
    'widgetItem' => '.room-item',
    'limit' => 4,
    'min' => 1,
    'insertButton' => '.add-room',
    'deleteButton' => '.remove-room',
    'model' => $modelsRoom[0],
    'formId' => 'dynamic-form',
    'formFields' => [
        'description'
    ],
]); ?>
<table class="table table-bordered">
    <thead>
        <tr>
            <th>Description</th>
            <th class="text-center">
                <button type="button" class="add-room btn btn-success btn-xs"><span class="glyphicon glyphicon-plus"></span></button>
            </th>
        </tr>
    </thead>
    <tbody class="container-rooms">
    <?php foreach ($modelsRoom as $indexRoom => $modelRoom): ?>
        <tr class="room-item">
            <td class="vcenter">
                <?php
                    // necessary for update action.
                    if (! $modelRoom->isNewRecord) {
                        echo Html::activeHiddenInput($modelRoom, "[{$indexHouse}][{$indexRoom}]id");
                    }
                ?>
                <?= $form->field($modelRoom, "[{$indexHouse}][{$indexRoom}]description")->label(false)->textInput(['maxlength' => true]) ?>
            </td>
            <td class="text-center vcenter" style="width: 90px;">
                <button type="button" class="remove-room btn btn-danger btn-xs"><span class="glyphicon glyphicon-minus"></span></button>
            </td>
        </tr>
     <?php endforeach; ?>
    </tbody>
</table>
<?php DynamicFormWidget::end(); ?>

7. Waktunya mencoba, semoga berhasil

form-update

#Jangan komplain kalo ada error :)

 

15 Comments
  1. Masyar
    • Hafid Mukhlasin
  2. Moz
    • Hafid Mukhlasin
  3. Kukur
    • Sunarno
  4. Adit
    • Hafid Mukhlasin
  5. nobody
  6. paulima
    • Hafid Mukhlasin
  7. Rino
  8. dian
  9. ilham

Leave a Reply

Your email address will not be published. Required fields are marked *