dev-master
9999999-dev https://github.com/yii-joblo/multimodelformYii extension for handling multiple records and models in a form
BSD-3-Clause
The Requires
- php >=5.2.0
by Joe Blocher
yii multimodel
Yii extension for handling multiple records and models in a form
This extension allows to work with multiple records and different models in a edit form. It handles clientside cloning and removing input elements/fieldsets and serverside batchInsert/Update/Delete., (*1)
Creating forms with this functionality is a 'tricky' workaround in views and controllers. This widget should do the main part of the work for you. It can be useful for order/registration forms ..., (*2)
Find the latest releases on github, (*3)
Not every form input element is supported., (*4)
You can use the most basic CFormInputElements (text, textarea, dropdownlist, ...)., (*5)
Some input widgets need a workaround with js-code after clientside cloning. Currently supported with ready to use javascript code in methods:, (*6)
Special handling:, (*7)
New in v6.0.0, (*8)
are not supported., (*9)
You can find the implemtation explained below in the demo application., (*10)
Assume you have two models 'group' (id, title) and 'member' (id, groupid, firstname,lastname,membersince). The id attribute is the autoincrement primary key., (*11)
Generate the models 'Group' and 'Member' with gii. For testing the error summary set the members firstname/lastname as required in the rules., (*12)
Generate the 'GroupController' and the group/views with gii. You don't need to create a 'MemberController' and the member views for this example., (*13)
Change the default actionUpdate of the GroupController to, (*14)
[php] public function actionUpdate($id) { Yii::import('ext.multimodelform.MultiModelForm'); $model=$this->loadModel($id); //the Group model $member = new Member; $validatedMembers = array(); //ensure an empty array if(isset($_POST['Group'])) { $model->attributes=$_POST['Group']; //the value for the foreign key 'groupid' $masterValues = array ('groupid'=>$model->id); if( //Save the master model after saving valid members MultiModelForm::save($member,$validatedMembers,$deleteMembers,$masterValues) && $model->save() ) $this->redirect(array('view','id'=>$model->id)); } $this->render('update',array( 'model'=>$model, //submit the member and validatedItems to the widget in the edit form 'member'=>$member, 'validatedMembers' => $validatedMembers, )); }
[php] public function actionCreate() { Yii::import('ext.multimodelform.MultiModelForm'); $model = new Group; $member = new Member; $validatedMembers = array(); //ensure an empty array if(isset($_POST['Group'])) { $model->attributes=$_POST['Group']; if( //validate detail before saving the master MultiModelForm::validate($member,$validatedMembers,$deleteItems) && $model->save() ) { //the value for the foreign key 'groupid' $masterValues = array ('groupid'=>$model->id); if (MultiModelForm::save($member,$validatedMembers,$deleteMembers,$masterValues)) $this->redirect(array('view','id'=>$model->id)); } } $this->render('create',array( 'model'=>$model, //submit the member and validatedItems to the widget in the edit form 'member'=>$member, 'validatedMembers' => $validatedMembers, )); }
[php] echo $this->renderPartial('_form', array('model'=>$model, 'member'=>$member,'validatedMembers'=>$validatedMembers));
[php] <div class="form wide"> <?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'group-form', 'enableAjaxValidation'=>false, )); ?> <p class="note">Fields with <span class="required">*</span> are required.</p> <?php //show errorsummary at the top for all models //build an array of all models to check echo $form->errorSummary(array_merge(array($model),$validatedMembers)); ?> <div class="row"> <?php echo $form->labelEx($model,'title'); ?> <?php echo $form->textField($model,'title'); ?> <?php echo $form->error($model,'title'); ?> </div> <?php // see http://www.yiiframework.com/doc/guide/1.1/en/form.table // Note: Can be a route to a config file too, // or create a method 'getMultiModelForm()' in the member model $memberFormConfig = array( 'elements'=>array( 'firstname'=>array( 'type'=>'text', 'maxlength'=>40, ), 'lastname'=>array( 'type'=>'text', 'maxlength'=>40, ), 'membersince'=>array( 'type'=>'dropdownlist', //it is important to add an empty item because of new records 'items'=>array(''=>'-',2009=>2009,2010=>2010,2011=>2011,), ), )); $this->widget('ext.multimodelform.MultiModelForm',array( 'id' => 'id_member', //the unique widget id 'formConfig' => $memberFormConfig, //the form configuration array 'model' => $member, //instance of the form model //if submitted not empty from the controller, //the form will be rendered with validation errors 'validatedItems' => $validatedMembers, //array of member instances loaded from db 'data' => $member->findAll('groupid=:groupId', array(':groupId'=>$model->id)), )); ?> <div class="row buttons"> <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?> </div> <?php $this->endWidget(); ?> </div><!-- form -->
You can split the validate() and save() methods of the Multimodelform to modify the items before saving., (*15)
[php] //validate formdata and populate $validatedItems/$deleteItems if (MultiModelForm::validate($model,$validatedItems,$deleteItems,$masterValues)) { //... alter the model attributes of $validatedItems if you need ... //will not execute internally validate again, because $validatedItems/$deleteItems are not empty MultiModelForm::save($model,$validatedItems,$deleteItems,$masterValues); }
Of course you can save() without extra validate before. Validation will be internally done when $validatedItems/$deleteItems are empty., (*16)
[php] $validatedItems=array(); if(isset($_POST['FORMDATA']) && MultiModelForm::save($model,$validatedItems,$deleteItems,$masterValues)) { //... validation and saving is ok ... //redirect ... } //No POST data or validation error on save $this->render ...
Set the property 'tableView'=>true., (*17)
[php] $this->widget('ext.multimodelform.MultiModelForm',array( ... 'tableView' => true, //'tableFootCells' => array('footerCol1','footerCol2'...), //optional table footer ... ));
Yii allows type='AWidget' as form element. So in your form config array you can use:, (*18)
[php] array( 'elements'=>array( .... 'lastname'=>array( 'type'=>'text', 'maxlength'=>40, ), 'dayofbirth'=>array( 'type'=>'zii.widgets.jui.CJuiDatePicker', 'language'=>'de', 'options'=>array( 'showAnim'=>'fold', ), ... ) )
This however needs a javascript workaround on cloning the date element. We have to assign the CJuiDatePicker functionality to the cloned new element., (*19)
There are the properties jsBeforeClone,jsAfterClone,jsBeforeNewId,jsAfterNewId available where javascript code can be implemented. Use 'this' as the current jQuery object., (*20)
For CJuiDatePicker, the extension datetimepicker, CJuiAutoComplete and EJuiComboBox there are predefined functions available, so it's easy to make cloning date fields work., (*21)
You have to assign the property 'jsAfterNewId' to the prepared code., (*22)
Assume your form definition elements are defined in the array $formConfig. 'afterNewIdDatePicker' reads the options from the specified element and adds the 'datepicker':, (*23)
[php] $this->widget('ext.multimodelform.MultiModelForm',array( ... // 'jsBeforeNewId' => "alert(this.attr('id'));", 'jsAfterNewId' => MultiModelForm::afterNewIdDatePicker($formConfig['elements']['dayofbirth']), ... ));
Now cloning of the field 'dayofbirth' should work., (*24)
Support for, (*25)
For other widgets you have to find out the correct javascript code on cloning. Please let me know if you have found a javascript code for other widgets., (*26)
Set the property 'sortAttribute' to your db-field for sorting (should be an integer) if you want to order the items by drag/drop manually. Uses jQuery UI sortable, but works only when 'tableView' is false. See the demo., (*27)
[php] $this->widget('ext.multimodelform.MultiModelForm',array( ... 'sortAttribute' => 'position', //if assigned: sortable fieldsets is enabled ... ));
Unfortunatly the basic checkbox is not supported, because it's not so easy to handle (see the comments in the forum)., (*28)
But if you need checkboxes, you can use the checkboxlist instead. If you only need a single checkbox you can set the item-data to an array with one item., (*29)
In the view / formConfig:, (*30)
[php] $memberFormConfig = array( 'elements'=>array( ... 'flags'=>array( 'type'=>'checkboxlist', 'items'=>array('1'=>'Founder','2'=>'Developer','3'=>'Marketing'), //One single checkbox: array('1'=>'Founder') ), ));
In the model you have to convert array string on saving/loading - see the Member model in the demo, (*31)
[php] //Convert the flags array to string public function beforeSave() { if(parent::beforeSave()) { if(!empty($this->flags) && is_array($this->flags)) $this->flags = implode(',',$this->flags); return true; } return false; } //Convert the flags string to array public function afterFind() { $this->flags = empty($this->flags) ? array() : explode(',',$this->flags); }
MultiModelForm handles all the stuff with uploading files and images. Assigned images will be displayed as preview, files as download link in the edit form near the upload input., (*32)
Add a input element of type 'file' to the form config. It's important to add 'visible'=>true because by Yii default a file field is not safe and unsafe attributes will not be visible., (*33)
[php] $memberFormConfig = array( 'elements'=>array( 'firstname'=>array( 'type'=>'text', 'maxlength'=>40, ), ... 'image'=>array( 'type'=>'file', 'visible'=>true, //Important! ), ));
Add the image field to the rules of your model. The image is of type file, so a CFileValidator will be used and you can add allowed types, mimetypes, maxSize etc. It's important to set 'allowEmpty'=>true otherwise, the user will be enforced to always upload a new image on updating a record., (*34)
[php] public function rules() { return array( array('firstname, lastname', 'length', 'max'=>40), array('image', 'file', 'allowEmpty'=>true,'types'=>'jpg,gif,png'), ... ); }
The file input field in the db must be of type string: VARCHAR(200) or similar to save the relative file path., (*35)
The default behavior on uploading is, that mmf will save the uploaded file in the public webroot folder files/modelclass. 'modelclass' is the class of the multimodel (files/member/image1.jpg)., (*36)
The relative path of the uploaded file will be assigned to the file attribute (image, ...)., (*37)
Ensure the public folder files is writeable like the assets folder., (*38)
MMF takes care about unique filenames in the folder by adding index: filename-1.jpg, filename-2.jpg ... if necessary., (*39)
You can change this default behavior by adding callback methods to your mmf model:, (*40)
Add a method mmfFileDir() to you mmf model (member,...), (*41)
[php] public function mmfFileDir($attribute,$mmf) { return 'media/'.strtolower(get_class($this)); }
The $attribute can be used too, for example if there are multiple file fields in a model. $mmf is the MultiModelForm widget., (*42)
Maybe you need another behavior on file upload, for example you want to create image presets (resize, ...) on upload or save the file into the db., (*43)
Add a callback method mmfSaveUploadedFile() to the mmf model. The param $uploadedFile is a CUploadedFile instance., (*44)
[php] public function mmfSaveUploadedFile($attribute,$uploadedFile,$mmf) { if(!empty($uploadedFile)) { $uploadedFile->saveAs(...); ... resize or save to db or whatever ... $this->$attribute = ... path to the preset or other values you like ... } }
If you need access to the mastermodel (group,...) inside this callback methods, you have to assign the new param 'masterModel' in the controller action., (*45)
[php] public function actionUpdate($id) { $model=$this->loadModel($id); //the Group model $member = new Member; $validatedMembers = array(); //ensure an empty array if(isset($_POST['Group'])) { ... //the last param $model is the masterModel 'group'. if(MultiModelForm::save($member,$validatedMembers,$deleteMembers,$masterValues,$_POST['Member'],$model) ... } ... }
Now in your callback methods you can use $groupModel=$mmf->masterModel;, (*46)
MMF does not delete the uploaded files if items are removed by the user in the edit form. If you want to delete the file after removing items you have to code like below:, (*47)
[php] if (MultiModelForm::save($model,$validatedItems,$deleteItems,$masterValues)) { foreach($deleted as $deletedModel) { if(!empty($deletedModel->image) && is_file($deletedModel->image)) unlink($deletedModel->image); } }
Regarding to requests and workarounds in the forum topic:, (*48)
A user should not be able to add/clone items, when in error mode (model rules not passed successfully). Now you can set the property $showAddItemOnError to false to enable this behavior. See the demo., (*49)
For example, if you only want to allow an admin user to add new items or remove items, you can use these properties to display the addlink and the removelinks, (*50)
[php] $this->widget('ext.multimodelform.MultiModelForm',array( ... 'allowAddItem' => Yii::app()->user->isAdmin(), 'allowRemoveItem' => Yii::app()->user->hasRole('admin'), ... ));
Set the property 'bootstrapLayout'=true if you use Twitters Bootstrap CSS or one of the Yii bootstrap extensions., (*51)
The formelements/labels will be wrapped by 'control-group', 'controls', ... so that the multimodelform should be displayed correct., (*52)
If you need to modify the cloned elements or execute js-actions after cloning, you can assign a js function(newElem,sourceElem) as callback., (*53)
The callback will be executed for all inputs of a record row., (*54)
Usage:, (*55)
[php] // a js function in your view echo CHtml::script('function alertIds(newElem,sourceElem) { alert(newElem.attr("id")); alert(sourceElem.attr("id"));}' ); $this->widget('ext.multimodelform.MultiModelForm',array( ... 'jsAfterCloneCallback'=>'alertIds', ... ));
If you upgrade MultiModelForm, don't forget to delete the assets., (*56)
You can use multiple MultiModelForm widgets in a view, but the mmf models MUST be of different classes. Take care to assign a unique widget id when adding more multimodelform widgets., (*57)
Take a look at MultiModelForm.php for more options of the widget., (*58)
The widget never will render a form begin/end tag. So you have always to add $this->beginWidget('CActiveForm',...) ... $this->endWidget in the view., (*59)
The implementation of the class MultiModelEmbeddedForm needs review in upcoming Yii releases. In 1.1.6+ the only output of CActiveForm.init() and CActiveForm.run() is the form begin and end tag., (*60)
The extension should work for non activerecord models too: instances of CModel, CFormModel..., (*61)
Use Yii::app()->controller->action->id or $model->scenario to generate different forms for your needs (readonly fields on update ...), (*62)
multimodelform on github, (*63)
Forum topic multimodelform/jqrelcopy, (*64)
Tutorial Using Form Builder, (*65)
jQuery plugin RelCopy, (*66)
v4.0, (*67)
v3.3, (*68)
v3.0, (*69)
v2.2.1 Bugfix: Array elements need extra 'allEmpty' checkĀ , (*70)
Yii extension for handling multiple records and models in a form
BSD-3-Clause
yii multimodel