Search code examples
javascriptphpyii2active-form

active form multi model and multi attributes in Yii2


In my project, I have an aggregation between, let's say, a University model and a Department model: a university have at least one department, while every department belongs to only one university. I'd like to have a possibility to create a university model instance with some number of department model instances and the exact number of departments is not known in advance (but at least one must exist). So, when creating a university, I'd like to have a page with one default department and an "Add Department" button that would allow me by means of javascript to add any number of departments that I need.

The question is: how should I write the create view page using ActiveForm in order that my POST array has the following structure:

   "University" => ["name" => "Sorbonne", "city" => "Paris"],
   "Faculty" => [
       0 => ["name" => "Medicine", "dean" => "Person A"], 
       1 => ["name" => "Physics", "dean" => "Person B"],
       2 => ["name" => "Mathematics", "dean" => "Person C"],
       ...
   ]

that I then pass to Faculty::loadMultiple() method.

I've tried something like this

   $form = ActiveForm::begin();
   echo $form->field($university, 'name')->textInput();
   echo $form->field($university, 'city')->textInput();
   foreach ($faculties as $i => $faculty) {
       echo $form->field($faculty, "[$i]name")->textInput();
       echo $form->field($faculty, "[$i]dean")->textInput()
   }
   ActiveForm::end();

It works, but when adding new department by means of javascript (I just clone an html node that contains department input fields), I am forced to elaborate the numbers coming from variable $i of the above php script. And this is quite annoying.

Another possibility that I've tried was to get rid of variable $i and write something like

    $form = ActiveForm::begin();
    echo $form->field($university, 'name')->textInput();
    echo $form->field($university, 'city')->textInput();
    foreach ($faculties as $faculty) {
        echo $form->field($faculty, "[]name")->textInput();
        echo $form->field($faculty, "[]dean")->textInput()
    }
    ActiveForm::end();

In this way, cloning the corresponding node is very simple, but the generated POST array has wrong structure due to [] brackets.

Is it possible to modify the latter approach and to have the required structure of the POST array?


Solution

  • Use Yii2 dynamic form extension:

    Installation

    The preferred way to install this extension is through composer.

    Either run:

    composer require --prefer-dist wbraganca/yii2-dynamicform "dev-master"
    

    Or add to the require section of your composer.json file:

    "wbraganca/yii2-dynamicform": "dev-master"
    

    Demo page: Nested Dynamic Form

    Nested Dynamic Form Demo Source Code:

    EER Diagram

    Source Code - View: _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(); ?>
    

    Source Code - View: _form-rooms.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>
    

    Source Code - Controller

    <?php
    
    namespace app\modules\yii2extensions\controllers;
    
    use Yii;
    use yii\helpers\ArrayHelper;
    use yii\web\NotFoundHttpException;
    use yii\web\Response;
    use yii\widgets\ActiveForm;
    use app\base\Model;
    use app\base\Controller;
    use app\modules\yii2extensions\models\House;
    use app\modules\yii2extensions\models\Person;
    use app\modules\yii2extensions\models\Room;
    use app\modules\yii2extensions\models\query\PersonQuery;
    
    /**
    * DynamicformDemo3Controller implements the CRUD actions for Person model.
    */
    class DynamicformDemo3Controller extends Controller
    {
    /**
     * Lists all Person models.
     * @return mixed
     */
    public function actionIndex()
    {
        $searchModel = new PersonQuery();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
    
        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }
    
    /**
     * Displays a single Person model.
     * @param integer $id
     * @return mixed
     */
    public function actionView($id)
    {
        $model = $this->findModel($id);
        $houses = $model->houses;
    
        return $this->render('view', [
            'model' => $model,
            'houses' => $houses,
        ]);
    }
    
    /**
     * Creates a new Person model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     * @return mixed
     */
    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();
                    }
                }
            }
    
            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,
        ]);
    }
    
    /**
     * Updates an existing Person model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     */
    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
        ]);
    }
    
    /**
     * Deletes an existing Person model.
     * If deletion is successful, the browser will be redirected to the 'index' page.
     * @param integer $id
     * @return mixed
     */
    public function actionDelete($id)
    {
        $model = $this->findModel($id);
        $name = $model->first_name;
    
        if ($model->delete()) {
            Yii::$app->session->setFlash('success', 'Record  <strong>"' . $name . '"</strong> deleted successfully.');
        }
    
        return $this->redirect(['index']);
    }
    
    /**
     * Finds the Person model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return Person the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = Person::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
    }