simomatte/yii2-dynamicform
It is widget to yii2 framework to clone form elements in a nested manner, maintaining accessibility.
Installs: 9
Dependents: 1
Suggesters: 0
Stars: 0
Watchers: 0
Forks: 438
Open Issues: 0
Language:JavaScript
Type:yii2-extension
Requires
- yiisoft/yii2:~2.0.5
- symfony/css-selector:~2.8|~3.0
- symfony/dom-crawler:~2.8|~3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
It is widget to yii2 framework to clone form elements in a nested manner, maintaining accessibility.
Installation
The preferred way to install this extension is through composer.
Either run
php composer.phar require --prefer-dist wbraganca/yii2-dynamicform "*"
or add
"wbraganca/yii2-dynamicform": "*"
to the require section of your composer.json
file.
Demos
Usage
Hypothetical Scenario
The View
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; use wbraganca\dynamicform\DynamicFormWidget; ?> <div class="customer-form"> <?php $form = ActiveForm::begin(['id' => 'dynamic-form']); ?> <div class="row"> <div class="col-sm-6"> <?= $form->field($modelCustomer, 'first_name')->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-6"> <?= $form->field($modelCustomer, 'last_name')->textInput(['maxlength' => true]) ?> </div> </div> <div class="panel panel-default"> <div class="panel-heading"><h4><i class="glyphicon glyphicon-envelope"></i> Addresses</h4></div> <div class="panel-body"> <?php DynamicFormWidget::begin([ 'widgetContainer' => 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_] 'widgetBody' => '.container-items', // required: css class selector 'widgetItem' => '.item', // required: css class 'limit' => 4, // the maximum times, an element can be cloned (default 999) 'min' => 1, // 0 or 1 (default 1) 'insertButton' => '.add-item', // css class 'deleteButton' => '.remove-item', // css class 'model' => $modelsAddress[0], 'formId' => 'dynamic-form', 'formFields' => [ 'full_name', 'address_line1', 'address_line2', 'city', 'state', 'postal_code', ], ]); ?> <div class="container-items"><!-- widgetContainer --> <?php foreach ($modelsAddress as $i => $modelAddress): ?> <div class="item panel panel-default"><!-- widgetBody --> <div class="panel-heading"> <h3 class="panel-title pull-left">Address</h3> <div class="pull-right"> <button type="button" class="add-item btn btn-success btn-xs"><i class="glyphicon glyphicon-plus"></i></button> <button type="button" class="remove-item btn btn-danger btn-xs"><i class="glyphicon glyphicon-minus"></i></button> </div> <div class="clearfix"></div> </div> <div class="panel-body"> <?php // necessary for update action. if (! $modelAddress->isNewRecord) { echo Html::activeHiddenInput($modelAddress, "[{$i}]id"); } ?> <?= $form->field($modelAddress, "[{$i}]full_name")->textInput(['maxlength' => true]) ?> <div class="row"> <div class="col-sm-6"> <?= $form->field($modelAddress, "[{$i}]address_line1")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-6"> <?= $form->field($modelAddress, "[{$i}]address_line2")->textInput(['maxlength' => true]) ?> </div> </div><!-- .row --> <div class="row"> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]city")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]state")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]postal_code")->textInput(['maxlength' => true]) ?> </div> </div><!-- .row --> </div> </div> <?php endforeach; ?> </div> <?php DynamicFormWidget::end(); ?> </div> </div> <div class="form-group"> <?= Html::submitButton($modelAddress->isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?> </div>
Javascript Events
$(".dynamicform_wrapper").on("beforeInsert", function(e, item) { console.log("beforeInsert"); }); $(".dynamicform_wrapper").on("afterInsert", function(e, item) { console.log("afterInsert"); }); $(".dynamicform_wrapper").on("beforeDelete", function(e, item) { if (! confirm("Are you sure you want to delete this item?")) { return false; } return true; }); $(".dynamicform_wrapper").on("afterDelete", function(e) { console.log("Deleted item!"); }); $(".dynamicform_wrapper").on("limitReached", function(e, item) { alert("Limit reached"); });
The Controller (sample code)
<?php namespace app\controllers; use Yii; use app\models\Customer; use app\models\CustomerSearch; use app\models\Address; use yii\web\Controller; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; use app\base\Model; use yii\web\Response; use yii\widgets\ActiveForm; use yii\helpers\ArrayHelper; /** * CustomerController implements the CRUD actions for Customer model. */ class CustomerController extends Controller { ... /** * Creates a new Customer model. * If creation is successful, the browser will be redirected to the 'view' page. * @return mixed */ public function actionCreate() { $modelCustomer = new Customer; $modelsAddress = [new Address]; if ($modelCustomer->load(Yii::$app->request->post())) { $modelsAddress = Model::createMultiple(Address::classname()); Model::loadMultiple($modelsAddress, Yii::$app->request->post()); // ajax validation if (Yii::$app->request->isAjax) { Yii::$app->response->format = Response::FORMAT_JSON; return ArrayHelper::merge( ActiveForm::validateMultiple($modelsAddress), ActiveForm::validate($modelCustomer) ); } // validate all models $valid = $modelCustomer->validate(); $valid = Model::validateMultiple($modelsAddress) && $valid; if ($valid) { $transaction = \Yii::$app->db->beginTransaction(); try { if ($flag = $modelCustomer->save(false)) { foreach ($modelsAddress as $modelAddress) { $modelAddress->customer_id = $modelCustomer->id; if (! ($flag = $modelAddress->save(false))) { $transaction->rollBack(); break; } } } if ($flag) { $transaction->commit(); return $this->redirect(['view', 'id' => $modelCustomer->id]); } } catch (Exception $e) { $transaction->rollBack(); } } } return $this->render('create', [ 'modelCustomer' => $modelCustomer, 'modelsAddress' => (empty($modelsAddress)) ? [new Address] : $modelsAddress ]); } /** * Updates an existing Customer model. * If update is successful, the browser will be redirected to the 'view' page. * @param integer $id * @return mixed */ public function actionUpdate($id) { $modelCustomer = $this->findModel($id); $modelsAddress = $modelCustomer->addresses; if ($modelCustomer->load(Yii::$app->request->post())) { $oldIDs = ArrayHelper::map($modelsAddress, 'id', 'id'); $modelsAddress = Model::createMultiple(Address::classname(), $modelsAddress); Model::loadMultiple($modelsAddress, Yii::$app->request->post()); $deletedIDs = array_diff($oldIDs, array_filter(ArrayHelper::map($modelsAddress, 'id', 'id'))); // ajax validation if (Yii::$app->request->isAjax) { Yii::$app->response->format = Response::FORMAT_JSON; return ArrayHelper::merge( ActiveForm::validateMultiple($modelsAddress), ActiveForm::validate($modelCustomer) ); } // validate all models $valid = $modelCustomer->validate(); $valid = Model::validateMultiple($modelsAddress) && $valid; if ($valid) { $transaction = \Yii::$app->db->beginTransaction(); try { if ($flag = $modelCustomer->save(false)) { if (! empty($deletedIDs)) { Address::deleteAll(['id' => $deletedIDs]); } foreach ($modelsAddress as $modelAddress) { $modelAddress->customer_id = $modelCustomer->id; if (! ($flag = $modelAddress->save(false))) { $transaction->rollBack(); break; } } } if ($flag) { $transaction->commit(); return $this->redirect(['view', 'id' => $modelCustomer->id]); } } catch (Exception $e) { $transaction->rollBack(); } } } return $this->render('update', [ 'modelCustomer' => $modelCustomer, 'modelsAddress' => (empty($modelsAddress)) ? [new Address] : $modelsAddress ]); } ... }
Model Class
<?php namespace app\base; 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; } }
To zero or more elements (use the following code in your view file)
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; use wbraganca\dynamicform\DynamicFormWidget; ?> <div class="customer-form"> <?php $form = ActiveForm::begin(['id' => 'dynamic-form']); ?> <div class="row"> <div class="col-sm-6"> <?= $form->field($modelCustomer, 'first_name')->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-6"> <?= $form->field($modelCustomer, 'last_name')->textInput(['maxlength' => true]) ?> </div> </div> <?php DynamicFormWidget::begin([ 'widgetContainer' => 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_] 'widgetBody' => '.container-items', // required: css class selector 'widgetItem' => '.item', // required: css class 'limit' => 4, // the maximum times, an element can be added (default 999) 'min' => 0, // 0 or 1 (default 1) 'insertButton' => '.add-item', // css class 'deleteButton' => '.remove-item', // css class 'model' => $modelsAddress[0], 'formId' => 'dynamic-form', 'formFields' => [ 'full_name', 'address_line1', 'address_line2', 'city', 'state', 'postal_code', ], ]); ?> <div class="panel panel-default"> <div class="panel-heading"> <h4> <i class="glyphicon glyphicon-envelope"></i> Addresses <button type="button" class="add-item btn btn-success btn-sm pull-right"><i class="glyphicon glyphicon-plus"></i> Add</button> </h4> </div> <div class="panel-body"> <div class="container-items"><!-- widgetBody --> <?php foreach ($modelsAddress as $i => $modelAddress): ?> <div class="item panel panel-default"><!-- widgetItem --> <div class="panel-heading"> <h3 class="panel-title pull-left">Address</h3> <div class="pull-right"> <button type="button" class="remove-item btn btn-danger btn-xs"><i class="glyphicon glyphicon-minus"></i></button> </div> <div class="clearfix"></div> </div> <div class="panel-body"> <?php // necessary for update action. if (! $modelAddress->isNewRecord) { echo Html::activeHiddenInput($modelAddress, "[{$i}]id"); } ?> <?= $form->field($modelAddress, "[{$i}]full_name")->textInput(['maxlength' => true]) ?> <div class="row"> <div class="col-sm-6"> <?= $form->field($modelAddress, "[{$i}]address_line1")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-6"> <?= $form->field($modelAddress, "[{$i}]address_line2")->textInput(['maxlength' => true]) ?> </div> </div><!-- .row --> <div class="row"> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]city")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]state")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-4"> <?= $form->field($modelAddress, "[{$i}]postal_code")->textInput(['maxlength' => true]) ?> </div> </div><!-- .row --> </div> </div> <?php endforeach; ?> </div> </div> </div><!-- .panel --> <?php DynamicFormWidget::end(); ?> <div class="form-group"> <?= Html::submitButton($modelAddress->isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?> </div>