yii2-dynamicform

Source Code - (Demo 2: File Upload)

EER Diagram

EER Diagram

Source Code - View: _form.php

<?php

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

?>

<div class="customer-form">
    <?php $form = ActiveForm::begin([
        'enableClientValidation' => false,
        'enableAjaxValidation' => true,
        'validateOnChange' => true,
        'validateOnBlur' => false,
        'options' => [
            'enctype' => 'multipart/form-data',
            'id' => 'dynamic-form'
        ]
    ]); ?>

    <?= $form->field($modelCatalogOption, 'name')->textInput(['maxlength' => true]) ?>

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

    <?= $this->render('_form_option_values', [
        'form' => $form,
        'modelCatalogOption' => $modelCatalogOption,
        'modelsOptionValue' => $modelsOptionValue
    ]) ?>

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

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

Source Code - View: _form_option_values.php

<?php

use yii\helpers\Html;
use yii\helpers\Url;
use yii\jui\JuiAsset;
use yii\web\JsExpression;
use kartik\widgets\FileInput;
use app\modules\yii2extensions\models\Image;
use wbraganca\dynamicform\DynamicFormWidget;

?>

<div id="panel-option-values" class="panel panel-default">
    <div class="panel-heading">
        <h3 class="panel-title"><i class="fa fa-check-square-o"></i> Option values</h3>
    </div>

    <?php DynamicFormWidget::begin([
        'widgetContainer' => 'dynamicform_wrapper',
        'widgetBody' => '.form-options-body',
        'widgetItem' => '.form-options-item',
        'min' => 1,
        'insertButton' => '.add-item',
        'deleteButton' => '.delete-item',
        'model' => $modelsOptionValue[0],
        'formId' => 'dynamic-form',
        'formFields' => [
            'name',
            'img'
        ],
    ]); ?>

    <table class="table table-bordered table-striped margin-b-none">
        <thead>
            <tr>
                <th style="width: 90px; text-align: center"></th>
                <th class="required">Option value name</th>
                <th style="width: 188px;">Image</th>
                <th style="width: 90px; text-align: center">Actions</th>
            </tr>
        </thead>
        <tbody class="form-options-body">
            <?php foreach ($modelsOptionValue as $index => $modelOptionValue): ?>
                <tr class="form-options-item">
                    <td class="sortable-handle text-center vcenter" style="cursor: move;">
                        <i class="fa fa-arrows"></i>
                    </td>
                    <td class="vcenter">
                        <?= $form->field($modelOptionValue, "[{$index}]name")->label(false)->textInput(['maxlength' => 128]); ?>
                    </td>
                    <td>
                        <?php if (!$modelOptionValue->isNewRecord): ?>
                            <?= Html::activeHiddenInput($modelOptionValue, "[{$index}]id"); ?>
                            <?= Html::activeHiddenInput($modelOptionValue, "[{$index}]image_id"); ?>
                            <?= Html::activeHiddenInput($modelOptionValue, "[{$index}]deleteImg"); ?>
                        <?php endif; ?>
                         <?php
                            $modelImage = Image::findOne(['id' => $modelOptionValue->image_id]);
                            $initialPreview = [];
                            if ($modelImage) {
                                $pathImg = Yii::$app->fileStorage->baseUrl . '/' . $modelImage->path;
                                $initialPreview[] = Html::img($pathImg, ['class' => 'file-preview-image']);
                            }
                        ?>
                        <?= $form->field($modelOptionValue, "[{$index}]img")->label(false)->widget(FileInput::classname(), [
                            'options' => [
                                'multiple' => false,
                                'accept' => 'image/*',
                                'class' => 'optionvalue-img'
                            ],
                            'pluginOptions' => [
                                'previewFileType' => 'image',
                                'showCaption' => false,
                                'showUpload' => false,
                                'browseClass' => 'btn btn-default btn-sm',
                                'browseLabel' => ' Pick image',
                                'browseIcon' => '<i class="glyphicon glyphicon-picture"></i>',
                                'removeClass' => 'btn btn-danger btn-sm',
                                'removeLabel' => ' Delete',
                                'removeIcon' => '<i class="fa fa-trash"></i>',
                                'previewSettings' => [
                                    'image' => ['width' => '138px', 'height' => 'auto']
                                ],
                                'initialPreview' => $initialPreview,
                                'layoutTemplates' => ['footer' => '']
                            ]
                        ]) ?>
                       
                    </td>
                    <td class="text-center vcenter">
                        <button type="button" class="delete-item btn btn-danger btn-xs"><i class="fa fa-minus"></i></button>
                    </td>
                </tr>
            <?php endforeach; ?>
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3"></td>
                <td><button type="button" class="add-item btn btn-success btn-sm"><span class="fa fa-plus"></span> New</button></td>
            </tr>
        </tfoot>
    </table>
    <?php DynamicFormWidget::end(); ?>
</div>

<?php
$js = <<<'EOD'

$(".optionvalue-img").on("filecleared", function(event) {
    var regexID = /^(.+?)([-\d-]{1,})(.+)$/i;
    var id = event.target.id;
    var matches = id.match(regexID);
    if (matches && matches.length === 4) {
        var identifiers = matches[2].split("-");
        $("#optionvalue-" + identifiers[1] + "-deleteimg").val("1");
    }
});

var fixHelperSortable = function(e, ui) {
    ui.children().each(function() {
        $(this).width($(this).width());
    });
    return ui;
};

$(".form-options-body").sortable({
    items: "tr",
    cursor: "move",
    opacity: 0.6,
    axis: "y",
    handle: ".sortable-handle",
    helper: fixHelperSortable,
    update: function(ev){
        $(".dynamicform_wrapper").yiiDynamicForm("updateContainer");
    }
}).disableSelection();

EOD;

JuiAsset::register($this);
$this->registerJs($js);
?>

Source Code - Controller

<?php

namespace app\modules\yii2extensions\controllers;

use Yii;
use yii\filters\VerbFilter;
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\CatalogOption;
use app\modules\yii2extensions\models\Image;
use app\modules\yii2extensions\models\OptionValue;
use app\modules\yii2extensions\models\query\CatalogOptionQuery;

/**
 * DynamicformDemo1Controller implements the CRUD actions for CatalogOption model.
 */
class DynamicformDemo2Controller extends Controller
{
    /**
     * Lists all CatalogOption models.
     * @return mixed
     */
    public function actionIndex()
    {
        $searchModel = new CatalogOptionQuery();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

    /**
     * Displays a single CatalogOption model.
     * @param integer $id
     * @return mixed
     */
    public function actionView($id)
    {
        $model = $this->findModel($id);
        $optionValues = $model->optionValues;

        return $this->render('view', [
            'model' => $model,
            'optionValues' => $optionValues,
        ]);
    }

    /**
     * Creates a new CatalogOption model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     * @return mixed
     */
    public function actionCreate()
    {
        $modelCatalogOption = new CatalogOption;
        $modelsOptionValue = [new OptionValue];
        if ($modelCatalogOption->load(Yii::$app->request->post())) {

            $modelsOptionValue = Model::createMultiple(OptionValue::classname());
            Model::loadMultiple($modelsOptionValue, Yii::$app->request->post());
            foreach ($modelsOptionValue as $index => $modelOptionValue) {
                $modelOptionValue->sort_order = $index;
                $modelOptionValue->img = \yii\web\UploadedFile::getInstance($modelOptionValue, "[{$index}]img");
            }

            // ajax validation
            if (Yii::$app->request->isAjax) {
                Yii::$app->response->format = Response::FORMAT_JSON;
                return ArrayHelper::merge(
                    ActiveForm::validateMultiple($modelsOptionValue),
                    ActiveForm::validate($modelCatalogOption)
                );
            }

            // validate all models
            $valid = $modelCatalogOption->validate();
            $valid = Model::validateMultiple($modelsOptionValue) && $valid;

            if ($valid) {
                $transaction = \Yii::$app->db->beginTransaction();
                try {
                    if ($flag = $modelCatalogOption->save(false)) {
                        foreach ($modelsOptionValue as $modelOptionValue) {
                            $modelOptionValue->catalog_option_id = $modelCatalogOption->id;

                            if (($flag = $modelOptionValue->save(false)) === false) {
                                $transaction->rollBack();
                                break;
                            }
                        }
                    }
                    if ($flag) {
                        $transaction->commit();
                        return $this->redirect(['view', 'id' => $modelCatalogOption->id]);
                    }
                } catch (Exception $e) {
                    $transaction->rollBack();
                }
            }
        }

        return $this->render('create', [
            'modelCatalogOption' => $modelCatalogOption,
            'modelsOptionValue' => (empty($modelsOptionValue)) ? [new OptionValue] : $modelsOptionValue
        ]);
    }

    /**
     * Updates an existing CatalogOption model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     */
    public function actionUpdate($id)
    {
        $modelCatalogOption = $this->findModel($id);
        $modelsOptionValue = $modelCatalogOption->optionValues;

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

            $oldIDs = ArrayHelper::map($modelsOptionValue, 'id', 'id');
            $modelsOptionValue = Model::createMultiple(OptionValue::classname(), $modelsOptionValue);
            Model::loadMultiple($modelsOptionValue, Yii::$app->request->post());
            $deletedIDs = array_diff($oldIDs, array_filter(ArrayHelper::map($modelsOptionValue, 'id', 'id')));

            foreach ($modelsOptionValue as $index => $modelOptionValue) {
                $modelOptionValue->sort_order = $index;
                $modelOptionValue->img = \yii\web\UploadedFile::getInstance($modelOptionValue, "[{$index}]img");
            }

            // ajax validation
            if (Yii::$app->request->isAjax) {
                Yii::$app->response->format = Response::FORMAT_JSON;
                return ArrayHelper::merge(
                    ActiveForm::validateMultiple($modelsOptionValue),
                    ActiveForm::validate($modelCatalogOption)
                );
            }

            // validate all models
            $valid = $modelCatalogOption->validate();
            $valid = Model::validateMultiple($modelsOptionValue) && $valid;

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

                        if (!empty($deletedIDs)) {
                            $flag = OptionValue::deleteByIDs($deletedIDs);
                        }

                        if ($flag) {
                            foreach ($modelsOptionValue as $modelOptionValue) {
                                $modelOptionValue->catalog_option_id = $modelCatalogOption->id;
                                if (($flag = $modelOptionValue->save(false)) === false) {
                                    $transaction->rollBack();
                                    break;
                                }
                            }
                        }
                    }

                    if ($flag) {
                        $transaction->commit();
                        return $this->redirect(['view', 'id' => $modelCatalogOption->id]);
                    }

                } catch (Exception $e) {
                    $transaction->rollBack();
                }
            }
        }

        return $this->render('update', [
            'modelCatalogOption' => $modelCatalogOption,
            'modelsOptionValue' => (empty($modelsOptionValue)) ? [new OptionValue] : $modelsOptionValue
        ]);
    }

    /**
     * Deletes an existing CatalogOption 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);
        $optonValuesIDs = ArrayHelper::map($model->optionValues, 'id', 'id');
        OptionValue::deleteByIDs($optonValuesIDs);
        $name = $model->name;

        if ($model->delete()) {
            Yii::$app->session->setFlash('success', 'Record  <strong>"' . $name . '"</strong> deleted successfully.');
        }

        return $this->redirect(['index']);
    }

    /**
     * Finds the CatalogOption model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return CatalogOption the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = CatalogOption::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
}