* @copyright 2011 myticket it-solutions gmbh * @license New BSD License * @category User Interface * @version 3.0 */ class MultiModelForm extends CWidget { const CLASSPREFIX = 'mmf_'; //prefix for tag classes /** * The model to handle * * @var CModel $model */ public $model; /** * Configuration of the form provided by the models method getMultiModelForm() * * This configuration array defines generation CForm * Can be a config array or a config file that returns the configuration * * @link http://www.yiiframework.com/doc/guide/1.1/en/form.builder * @var mixed $elements */ public $formConfig = array(); /** * Array of models loaded from db. * Created for example by $model->findAll(); * * @var CModel $data */ public $data; /** * The controller returns all validated items (array of model) * if a validation error occurs. * The form will then be rendered with error output. * $data will be ignored in this case. * @see method run() * * @var array $validatedItems */ public $validatedItems; /** * Set to true if the error summary should be rendered for the model of this form * * @var boolean $showErrorSummary */ public $showErrorSummary = false; /** * The text of the copy/clone link * * @var string $addItemText */ public $addItemText = 'Add item'; /** * Show 'Add item' link and empty item in errormode * * @var boolean $allowAddOnError */ public $showAddItemOnError = true; /** * The text for the remove link * Can be an image tag too. * Leave empty to disable removing. * * @var string $removeText */ public $removeText = 'Remove'; /** * The confirmation text before remove an item * Set to null/empty to disable confirmation * * @var string $removeText */ public $removeConfirm = 'Delete this item?'; /** * The htmlOptions for the remove link * * @var array $removeHtmlOptions */ public $removeHtmlOptions = array(); /** * Show elements as table * If set to true, $fieldsetWrapper, $rowWrapper and $removeLinkWrapper will be ignored * * @var boolean */ public $tableView = false; /** * The htmlOptions for the table tag * * @var array $tableHtmlOptions */ public $tableHtmlOptions = array(); /** * Items are rendered as Item1Item2 ... * * @var string $tableFootCells */ public $tableFootCells = array(); /** * Set this attribute to enable manual sorting by drag/drop of the multiple items * Uses the CJuiSortable widget * * @var string the name of the attribute */ public $sortAttribute; /** * The options property of the zii.widgets.jui.CJuiSortable * * @link http://www.yiiframework.com/doc/api/1.1/CJuiWidget#options-detail * @link http://jqueryui.com/demos/sortable/ * * @var array */ public $sortOptions = array( 'placeholder' => 'ui-state-highlight', 'opacity' => 0.8, 'cursor' => 'move' ); /** * The wrapper for each fieldset * * @var array $fieldsetWrapper */ public $fieldsetWrapper = array( 'tag' => 'div', 'htmlOptions' => array('class' => 'view'), //'fieldset' is unknown in the default css context of form.css ); /** * The wrapper for a row * * @var array $rowWrapper */ public $rowWrapper = array( 'tag' => 'div', 'htmlOptions' => array('class' => 'row'), ); /** * The wrapper for the removeLink * * @var array $fieldsetWrapper */ public $removeLinkWrapper = array( 'tag' => 'span', 'htmlOptions' => array(), ); /** * The javascript code jsBeforeClone,jsAfterClone ... * This allows to handle widgets on cloning. * Important: 'this' is the current handled jQuery object * For CJuiDatePicker and extension 'datetimepicker' see prepared php-code below: afterNewIdDatePicker,afterNewIdDateTimePicker * * Usage if you have CJuiDatePicker to clone (assume your form elements are defined in the array $formConfig): * 'jsAfterNewId' => MultiModelForm::afterNewIdDateTimePicker($formConfig['elements']['mydatefield']), * */ public $jsBeforeClone; // 'jsBeforeClone' => "alert(this.attr('class'));"; public $jsAfterClone; // 'jsAfterClone' => "alert(this.attr('class'));"; public $jsBeforeNewId; // 'jsBeforeNewId' => "alert(this.attr('id'));"; public $jsAfterNewId; // 'jsAfterNewId' => "alert(this.attr('id'));"; /** * Available options for the jQuery plugin RelCopy * * string excludeSelector - A jQuery selector used to exclude an element and its children * integer limit - The number of allowed copies. Default: 0 is unlimited * string append - Additional HTML to attach at the end of each copy. * string copyClass - A class to attach to each copy * boolean clearInputs - Option to clear each copies text input fields or textarea * * @link http://www.andresvidal.com/labs/relcopy.html * * @var array $options */ public $options = array(); /** * The assets url * * @var string $_assets */ private $_assets; /** * Support for CJuiDatePicker * Set 'jsAfterNewId'=MultiModelForm::afterNewIdDateTimePicker($myFormConfig['elements']['mydate']) * if you use at least one datepicker. * * The options will be assigned from the config array of the element * * @param array $element * @return string */ public static function afterNewIdDatePicker($element) { $options = isset($element['options']) ? $element['options'] : array(); $jsOptions = CJavaScript::encode($options); $language = isset($element['language']) ? $element['language'] : ''; if (!empty($language)) $language = "jQuery.datepicker.regional['$language'],"; return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker'); this.datepicker(jQuery.extend({showMonthAfterYear:false}, $language {$jsOptions}));};"; } /** * Support for extension datetimepicker * @link http://www.yiiframework.com/extension/datetimepicker/ * * @param array $element * @return string */ public static function afterNewIdDateTimePicker($element) { $options = isset($element['options']) ? $element['options'] : array(); $jsOptions = CJavaScript::encode($options); $language = isset($element['language']) ? $element['language'] : ''; if (!empty($language)) $language = "jQuery.datepicker.regional['$language'],"; return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker').datetimepicker(jQuery.extend($language {$jsOptions}));};"; } /** * Support for CJuiAutoComplete: not working - needs review * * @param array $element * @return string */ public static function afterNewIdAutoComplete($element) { $options = isset($element['options']) ? $element['options'] : array(); if (isset($element['sourceUrl'])) $options['source'] = CHtml::normalizeUrl($element['sourceUrl']); else $options['source'] = $element['source']; $jsOptions = CJavaScript::encode($options); //return "this.autocomplete($jsOptions);"; //works for non-autocomplete elements //return "if(this.hasClass('ui-autocomplete-input')) this.autocomplete($jsOptions);"; //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete($jsOptions);"; //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete('destroy').autocomplete($jsOptions);"; //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);"; //return "if(this.hasClass('ui-autocomplete-input')) this.unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);"; } /** * This static function should be used in the controllers update action * The models will be validated before saving * * If a record is not valid, the invalid model will be set to $model * to display error summary * * @param mixed $model CActiveRecord or other CModel * @param array $validatedItems returns the array of validated records * @param array $deleteItems * @param array $masterValues attributes to assign before saving * @param array $formData (default = $_POST) * @return boolean */ public static function save($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null) { //validate if empty: means no validation has been done $doValidate = empty($validatedItems) && empty($deleteItems); if (!isset($formData)) $formData = $_POST; $sortAttribute = !empty($formData[self::CLASSPREFIX . 'sortAttribute']) ? $formData[self::CLASSPREFIX . 'sortAttribute'] : null; $sortIndex = 0; if ($doValidate) { //validate and assign $masterValues if (!self::validate($model, $validatedItems, $deleteItems, $masterValues, $formData)) return false; } if (!empty($validatedItems)) foreach ($validatedItems as $item) { if (!$doValidate) //assign $masterValues { if (!empty($masterValues)) $item->setAttributes($masterValues, false); } //if sortable, assign the sortAttribute if (!empty($sortAttribute)) { $sortIndex++; $item->$sortAttribute = $sortIndex; } if (!$item->save()) return false; } //$deleteItems = array of primary keys to delete if (!empty($deleteItems)) foreach ($deleteItems as $pk) if (!empty($pk)) { //array doesn't work with activerecord? if (count($pk == 1)) { $vals = array_values($pk); $pk = $vals[0]; } $model->deleteByPk($pk); } return true; } /** * Validates submitted formdata * If a record is not valid, the invalid model will be set to $model * to display error summary * * @param mixed $model * @param array $validatedItems returns the array of validated records * @param array $deleteItems returns the array of model for deleting * @param array $masterValues attributes to assign before saving * @param array $formData (default = $_POST) * @return boolean */ public static function validate($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null) { $widget = new MultiModelForm; $widget->model = $model; $widget->checkModel(); if (!$widget->initItems($validatedItems, $deleteItems, $masterValues, $formData)) return false; //at least one item is not valid else return true; } /** * Converts the submitted formdata into an array of model * * @param array $formData the postdata $_POST submitted by the form * @param array $validatedItems the items which were validated * @param array $deleteItems the items to delete * @param array $masterValues assign additional masterdata before save * @return array array of model */ public function initItems(&$validatedItems, &$deleteItems, $masterValues = array(), $formData = null) { if (!isset($formData)) $formData = $_POST; $result = true; $newItems = array(); $validatedItems = array(); //bugfix: 1.0.2 $deleteItems = array(); $modelClass = get_class($this->model); if (!isset($formData) || empty($formData[$modelClass])) return true; //----------- NEW (on validation error) ----------- if (isset($formData[$modelClass]['n__'])) { foreach ($formData[$modelClass]['n__'] as $idx => $attributes) { $model = new $modelClass; $model->attributes = $attributes; if (!empty($masterValues)) $model->setAttributes($masterValues, false); //assign mastervalues // validate if (!$model->validate()) $result = false; $validatedItems[] = $model; } unset($formData[$modelClass]['n__']); } //----------- UPDATE ----------- $allExistingPk = isset($formData[$modelClass]['pk__']) ? $formData[$modelClass]['pk__'] : null; //bugfix: 1.0.1 if (isset($formData[$modelClass]['u__'])) { foreach ($formData[$modelClass]['u__'] as $idx => $attributes) { $model = new $modelClass('update'); //should work for CModel, mongodb models... too if (method_exists($model, 'setIsNewRecord')) $model->setIsNewRecord(false); $model->attributes = $attributes; if (!empty($masterValues)) $model->setAttributes($masterValues, false); //assign mastervalues //ensure to assign primary keys (when pk is unsafe or not defined in rules) if (is_array($allExistingPk)) { $primaryKeys = $allExistingPk[$idx]; $model->setAttributes($primaryKeys, false); } // validate if (!$model->validate()) $result = false; $validatedItems[] = $model; // remove from $allExistingPk if (is_array($allExistingPk)) unset($allExistingPk[$idx]); } unset($formData[$modelClass]['u__']); } //----------- DELETE ----------- // add remaining primarykeys to $deleteItems (reindex) if (is_array($allExistingPk)) foreach ($allExistingPk as $idx => $delPks) $deleteItems[] = $delPks; // remove handled formdata pk__ unset($formData[$modelClass]['pk__']); //----------- Check for cloned elements by jQuery ----------- if(!empty($formData[$modelClass])) //has cloned elements { // use the first item as reference $refAttribute = key($formData[$modelClass]); $refArray = array_shift($formData[$modelClass]); if (!empty($refArray)) foreach ($refArray as $idx => $value) { // check continue if all values are empty if (empty($value)) { $allEmpty = true; foreach ($formData[$modelClass] as $attrKey => $values) { if (is_array($values[$idx])) //bugfix v2.1.1 have to check empty array items too { $isEmpty = true; foreach ($formData[$modelClass][$attrKey] as $item) { if (!empty($item[$idx])) { $isEmpty = false; break; } } } else $isEmpty = empty($values[$idx]); $allEmpty = $isEmpty && $allEmpty; if (!$allEmpty) break; } if ($allEmpty) continue; } $model = new $modelClass; $model->$refAttribute = $value; foreach ($formData[$modelClass] as $attrKey => $values) { //v2.2 support for checkboxlist / radiolist if (is_array($values[$idx])) { $arrayAttribute = array(); foreach ($formData[$modelClass][$attrKey] as $item) { if (!empty($item[$idx])) $arrayAttribute[] = $item[$idx]; } $model->$attrKey = $arrayAttribute; } else $model->$attrKey = $values[$idx]; } //assign mastervalues without checking rules for new records $model->setAttributes($masterValues, false); // validate if (!$model->validate()) $result = false; $validatedItems[] = $model; } } return $result; } /** * Get the primary key as array (key => value) * * @param CModel $model * @return array */ public function getPrimaryKey($model) { $result = array(); if ($model instanceof CActiveRecord) { $pkValue = $model->primaryKey; if (!empty($pkValue)) { $pkName = $model->primaryKey(); if (empty($pkName)) $pkName = $model->tableSchema->primaryKey; $result = array($pkName => $pkValue); } } else // when working with EMongoDocument if (method_exists($model, 'primaryKey')) { $pkName = $model->primaryKey(); $pkValue = $model->$pkName; if (empty($pkValue)) $result = array($pkName => $pkValue); } return $result; } /** * Get the copyClass * * @return string */ public function getCopyClass() { if (isset($this->options['copyClass'])) return $this->options['copyClass']; else { $selector = $this->id . '_copy'; $this->options['copyClass'] = $selector; return $selector; } } /** * The link for removing a fieldset * * @return string */ public function getRemoveLink() { if (empty($this->removeText)) return ''; $onClick = 'jQuery(this).parent().parent().remove(); return false;'; if (!empty($this->removeConfirm)) $onClick = "if(confirm('{$this->removeConfirm}')) " . $onClick; $htmlOptions = array_merge($this->removeHtmlOptions, array('onclick' => $onClick)); $link = CHtml::link($this->removeText, '#', $htmlOptions); return CHtml::tag($this->removeLinkWrapper['tag'], $this->removeLinkWrapper['htmlOptions'], $link); } /** * Check if rows has to be sortable * Works only if not is as tableView because the submitted $_POST data are not in the correct sorted order * Sorting in tableView needs more investigation/workaround ... * * @return bool */ public function isSortable() { return !empty($this->sortAttribute) && !$this->tableView; } /** * Initialize the widget: register scripts */ public function init() { if ($this->tableView) { $this->fieldsetWrapper = array('tag' => 'tr', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'row')); $this->rowWrapper = array('tag' => 'td', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'cell')); $this->removeLinkWrapper = $this->rowWrapper; } $this->checkModel(); $this->registerClientScript(); parent::init(); } /** * Check the model instance on init / after create * Add all model attributes as hidden and visible=false if they are not part of the formConfig * Need this because on update all attributes have to be submitted, no 'loadModel' is called */ protected function checkModel() { if (is_string($this->model)) $this->model = new $this->model; if (isset($this->model) && isset($this->formConfig)) { // add undefined attributes in the form config as hidden fields and attribute visible = false if (isset($this->formConfig) && !empty($this->formConfig['elements'])) foreach ($this->model->attributes as $attribute => $value) { if (!array_key_exists($attribute, $this->formConfig['elements'])) $this->formConfig['elements'][$attribute] = array('type' => 'hidden', 'visible' => false); } } } /** * @return array the javascript options */ protected function getClientOptions() { if (empty($this->options)) $this->options = array(); if (!empty($this->removeText)) { $append = $this->getRemoveLink(); $this->options['append'] = empty($this->options['append']) ? $append : $append . ' ' . $this->options['append']; } if (!empty($this->jsBeforeClone)) $this->options['beforeClone'] = $this->jsBeforeClone; if (!empty($this->jsAfterClone)) $this->options['afterClone'] = $this->jsAfterClone; if (!empty($this->jsBeforeNewId)) $this->options['beforeNewId'] = $this->jsBeforeNewId; if (!empty($this->jsAfterNewId)) $this->options['afterNewId'] = $this->jsAfterNewId; return CJavaScript::encode($this->options); } /** * The id selector for jQuery.sortable * * @return string */ protected function getSortSelectorId() { return self::CLASSPREFIX . 'sortable'; } /** * Registers the relcopy javascript file. */ public function registerClientScript() { $cs = Yii::app()->getClientScript(); $this->_assets = Yii::app()->assetManager->publish(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets'); $cs->registerCoreScript('jquery'); $cs->registerScriptFile($this->_assets . '/js/jquery.relcopy.yii.1.0.js'); $options = $this->getClientOptions(); $cs->registerScript(__CLASS__ . '#' . $this->id, "jQuery('#{$this->id}').relCopy($options);"); //add the script for jQuery.sortable if ($this->isSortable()) { $cs->registerCoreScript('jquery.ui'); $cssFile = $cs->getCoreScriptUrl() . '/jui/css/base/jquery-ui.css'; $cs->registerCssFile($cssFile); $options = CJavaScript::encode($this->sortOptions); Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id . 'Sortable', "jQuery('#{$this->getSortSelectorId()}').sortable({$options});"); } } /** * Render the top of the table: AddLink, Table header */ public function renderTableBegin($renderAddLink) { $form = new MultiModelRenderForm($this->formConfig, $this->model); $form->parentWidget = $this; //add link as div if ($renderAddLink) { $addLink = $form->getAddLink(); echo CHtml::tag('div', array('class' => self::CLASSPREFIX . 'addlink'), $addLink); } $tableHtmlOptions = array_merge(array('class' => self::CLASSPREFIX . 'table'), $this->tableHtmlOptions); //table echo CHtml::tag('table', $tableHtmlOptions, false, false); //thead $form->renderTableHeader(); //tfoot if (!empty($this->tableFootCells)) { $cells = ''; foreach ($this->tableFootCells as $cell) { $cells .= CHtml::tag('td', array('class' => self::CLASSPREFIX . 'cell'), $cell); } $cells = CHtml::tag('tr', array('class' => self::CLASSPREFIX . 'row'), $cells); echo CHtml::tag('tfoot', array(), $cells); } //tbody $tbodyOptions = $this->isSortable() ? array('id' => $this->getSortSelectorId()) : array(); echo CHtml::tag('tbody', $tbodyOptions, false, false); } /** * Renders the active form if a model and formConfig is set * $this->data is array of model */ public function run() { if (empty($this->model) || empty($this->formConfig)) return; //form is displayed again with some invalid models $isErrorMode = !empty($this->validatedItems); $showAddLink = !$isErrorMode || ($isErrorMode && $this->showAddItemOnError); $this->formConfig['activeForm'] = array('class' => 'MultiModelEmbeddedForm'); $idx = 0; $errorPk = null; if ($isErrorMode) { if ($this->showErrorSummary) echo CHtml::errorSummary($this->validatedItems); $data = $this->validatedItems; } else $data = $this->data; //from the db if ($this->tableView) $this->renderTableBegin(false); //RODAX $showAddLink); if ($this->isSortable()) { //render the name of the sortAttribute as hidden input //used in MultiModelForm::save echo CHtml::hiddenField(self::CLASSPREFIX . 'sortAttribute', $this->sortAttribute); if (!$this->tableView) echo CHtml::openTag('div', array('id' => $this->getSortSelectorId())); } // existing records if (is_array($data) && !empty($data)) { foreach ($data as $model) { $form = new MultiModelRenderForm($this->formConfig, $model); $form->index = $idx; $form->parentWidget = $this; $form->primaryKey = $this->getPrimaryKey($model); if (!$this->tableView) { if ($showAddLink && $idx == 0) // no existing data rendered echo $form->renderAddLink(); } // render pk outside of removeable tag, for checking records to delete // see method initItems() echo $form->renderHiddenPk('[pk__]'); echo $form->render(); $idx++; } } //if form is displayed first time or in errormode and want to show 'Add item' (and a 'CopyTemplate') if($showAddLink) { // add an empty fieldset as CopyTemplate $form = new MultiModelRenderForm($this->formConfig, $this->model); $form->index = $idx; $form->parentWidget = $this; $form->isCopyTemplate = true; if (!$this->tableView) { if ($idx == 0) // no existing data rendered echo $form->renderAddLink(); } echo $form->render(); } if ($this->tableView) { echo CHtml::closeTag('tbody'); echo CHtml::closeTag('table'); } elseif ($this->isSortable()) echo CHtml::closeTag('div'); } } /** * The CForm to render the input form */ class MultiModelRenderForm extends CForm { public $parentWidget; public $index; public $isCopyTemplate; public $primaryKey; /** * Wraps a content with row wrapper * * @param string $content * @return string */ protected function getWrappedRow($content) { return CHtml::tag($this->parentWidget->rowWrapper['tag'], $this->parentWidget->rowWrapper['htmlOptions'], $content); } /** * Wraps a content with fieldset wrapper * * @param string $content * @return string */ protected function getWrappedFieldset($content) { return CHtml::tag($this->parentWidget->fieldsetWrapper['tag'], $this->parentWidget->fieldsetWrapper['htmlOptions'], $content); } /** * Returns the generated label from Yii form builder * Needs to be replaced by the real attributeLabel * @see method renderFormElements() * * @param string $prefix * @param string $attributeName * @return string */ protected function getAutoCreatedLabel($prefix, $attributeName) { return ($this->model->generateAttributeLabel('[' . $prefix . '][' . $this->index . ']' . $attributeName)); } /** * Renders the table head * * @return string */ public function renderTableHeader() { $cells = ''; foreach ($this->getElements() as $element) if ($element->visible) { $text = empty($element->label) ? ' ' : $element->label; $options = array(); if ($element->getRequired()) { $options = array('class' => CHtml::$requiredCss); $text .= CHtml::$afterRequiredLabel; } $cells .= CHtml::tag('th', $options, $text); } //add an empty column instead of remove link $cells .= CHtml::tag('th', array(), ' '); $row = $this->getWrappedFieldset($cells); echo CHtml::tag('thead', array(), $cells); } /** * Check if elem is a array type * * @param string $type * @return boolean */ protected function isElementArrayType($type) { switch ($type) { case 'checkboxlist': case 'radiolist': return true; default: return false; } // switch } /** * Renders a single form element * Remove the '[]' from the label * * @return string */ protected function renderFormElements() { $output = ''; $elements = $this->getElements(); foreach ($elements as $element) { if (isset($element->name)) { $elemName = $element->name; $elemLabel = $element->renderLabel(); //get the correct/nice label before changing name $element->label = ''; //no label on $element->render() if ($this->isCopyTemplate) // new fieldset { if ($element->visible) { //v.2.2 support for checkboxlist radiolist //Array types have to be rendered as array in the CopyTemplate $element->name = $this->isElementArrayType($element->type) ? $elemName . '[][]' : $elemName . '[]'; $elemOutput = ($element->type == 'hidden' || //bugfix: v2.1.1 $this->parentWidget->tableView) ? '' : $elemLabel; $elemOutput .= $element->render(); //bugfix: v2.1 - don't render hidden inputs in table cell $output .= $element->type == 'hidden' ? $elemOutput : $this->getWrappedRow($elemOutput); } } elseif (!empty($this->primaryKey)) { // existing fieldsets update $prefix = 'u__'; $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName; if ($element->type == 'hidden') $output .= $element->render(); else { $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel; $elemOutput .= $element->render(); $output .= $this->getWrappedRow($elemOutput); } } else { //in validation error mode: the new added items before if ($element->visible) { $prefix = 'n__'; $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName; if ($element->type == 'hidden') $output .= $element->render(); else { $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel; $elemOutput .= $element->render(); $output .= $this->getWrappedRow($elemOutput); } } } } } if (!$this->isCopyTemplate) $output .= $this->parentWidget->getRemoveLink(); return $output; } /** * Renders the primary key value as hidden field * Need determine which records to delete * * @param string $classSuffix * @return string */ public function renderHiddenPk($classSuffix = '[pk__]') { foreach ($this->primaryKey as $key => $value) { $modelClass = get_class($this->parentWidget->model); $name = $modelClass . $classSuffix . '[' . $this->index . ']' . '[' . $key . ']'; return CHtml::hiddenField($name, $value); } } /** * Get the add item link * * @return string */ public function getAddLink() { return CHtml::tag('a', array('id' => $this->parentWidget->id, 'href' => '#', 'rel' => '.' . $this->parentWidget->getCopyClass() ), $this->parentWidget->addItemText ); } /** * Renders the link 'Add' for cloning the DOM element * * @return string */ public function renderAddLink() { return $this->getWrappedRow($this->getAddLink()); } /** * Renders the CForm * Each fieldset is wrapped with the fieldsetWrapper * * @return string */ public function render() { $elemOutput = $this->renderBegin(); $elemOutput .= $this->renderFormElements(); $elemOutput .= $this->renderEnd(); // wrap $elemOutput $wrapperClass = $this->parentWidget->fieldsetWrapper['htmlOptions']['class']; if ($this->isCopyTemplate) { $class = empty($wrapperClass) ? $this->parentWidget->getCopyClass() : $wrapperClass . ' ' . $this->parentWidget->getCopyClass(); } else $class = $wrapperClass; $this->parentWidget->fieldsetWrapper['htmlOptions']['class'] = $class; return $this->getWrappedFieldset($elemOutput); } } /** * MultiModelEmbeddedForm * * A CActiveForm with no output of the form begin and close tag * In Yii 1.1.6 the form end/close is the only output of the methods init() and run() * Needs review in upcoming releases * */ class MultiModelEmbeddedForm extends CActiveForm { /** * Initializes the widget. * Don't render the open tag */ public function init() { ob_start(); parent::init(); ob_get_clean(); } /** * Runs the widget. * Don't render the close tag */ public function run() { ob_start(); parent::run(); ob_get_clean(); } }