I wondered if CForm would support tabular input. Out of the box it doesn’t, but some very minor changes allow it to do just that. Here’s what I have come up with:
Firstly an extension to CForm.
The changes are:
-
the addition of $_key to allow indexing into $_GET/POST[model]
-
change to the constructor to set the key if the form is for tabular input. Null means a normal form
-
addition of isTabular to determine if the form is for tabular input
-
change to loadData to index into the submitted data for tabular input
-
change to element rendering to produce a valid class
class EForm extends CForm {
/**
* @var boolean the key to fetch when the form is used for tabular input
*/
private $_key;
/**
* Constructor.
* $config, $model and $parent are as for CForm
* @param mixed key to fetch for the model on form submission when using tabular input
*/
public function __construct($config,$model,$parent=null,$key=null)
{
$this->_key=$key;
parent::__construct($config,$model,$parent);
}
/**
* The only difference here is to check if the form is used for tabular input.
* If it is load attributes from the appropriate index from the submitted data
*/
public function loadData()
{
if($this->_model!==null)
{
$class=get_class($this->_model);
if(strcasecmp($this->getRoot()->method,'get') && isset($_POST[$class]))
{
if($this->isTabular())
$this->_model->setAttributes($_POST[$class][$this->_key]);
else
$this->_model->setAttributes($_POST[$class]);
}
elseif(isset($_GET[$class]))
{
if($this->isTabular())
$this->_model->setAttributes($_GET[$class][$this->_key]);
else
$this->_model->setAttributes($_GET[$class]);
}
}
foreach($this->getElements() as $element)
{
if($element instanceof self)
$element->loadData();
}
}
/**
* Checks if the form is used for tabular input
* @return true if this form is used for tabular input, false if not
*/
public function isTabular()
{
return isset($this->_key);
}
/**
* Only one line is changed from CForm to render a valid class when
* using tabular inputs. The line is marked.
*/
public function renderElement($element)
{
if(is_string($element))
{
if(($e=$this[$element])===null && ($e=$this->getButtons()->itemAt($element))===null)
return $element;
else
$element=$e;
}
if($element->getVisible())
{
if($element instanceof CFormInputElement)
{
if($element->type==='hidden')
return "<div style=\"visibility:hidden\">\n".$element->render()."</div>\n";
else
{
$elementName = $element->name;
return '<div class="row field_' . strtolower(preg_replace('/(\[\w*\])?\[(\w*)\]/', '_$2', CHtml::resolveName($element->getParent()->getModel(), $elementName))) . "\">\n".$element->render()."</div>\n"; // This line is the change
}
}
elseif($element instanceof CFormButtonElement)
return $element->render()."\n";
else
return $element->render();
}
return '';
}
}
There are some changes required to isAttributeSafe and isAttributeRequired in CModel so that tabular inputs render, so I override them in the model class that uses tabular input.
All that is going on is to strip away the index portion from an attribute name in order to check the attribute, e.g. [index]attribute => attribute
public function isAttributeRequired($attribute) {
return parent::isAttributeRequired(preg_replace('/(\[\w+\])?(\w+)/', '$2', $attribute));
}
public function isAttributeSafe($attribute) {
return parent::isAttributeSafe(preg_replace('/(\[\w+\])?(\w+)/', '$2', $attribute));
}
And that is it. The changes - were they to be implemented in the core - are backward compatible.
Here’s a usage example.
In the application I am developing right now I have products that have a number of parameters; the relationship is many-many. So there is a product table, parameter table and product_parameter table. Which parameters are shown depends on the category of product.
In the product model, to create the form I do:
public function getForm() {
$form = new CForm($this->_form, $this);
$elements = $form->getElements();
$subForm = new CForm(array('elements' => array()), null, $form); // Sub-form to act as a container for the parameter forms.
$subForm->title = 'Parameters';// Title to make it a fieldset
$subFormElements = $subForm->getElements();
// NOTE:: parameters were set earlier as related models to the product
foreach ($this->parameters as $parameterId => $parameter)
$subFormElements->add($parameterId, $parameter->getForm($subForm));
$elements->add('parameters', $subForm);
return $form;
}
In the ProductParameter model I have:
public function getForm($parent) {
return new EForm(array(
'elements' => array(
"[{$this->parameterId}]value" => array(
'type' => 'text',
'label' => $this->parameterName,
'after' => $this->parameterUnits
)
)
), $this, $parent, $this->parameterId);
}
Note the format for the element name - [index]attribute, and that the index is passed to the EForm constructor. parameterId, Name and Units are from the parameter models applicable to the product.
This model is also where isAtributeSafe/Required are overridden.
I think this makes the CForm concept applicable to many more situations, and I hope this is of interest.