I'm currently doing some experiments with Zend forms and custom rendering and I was wondering how to handle form errors.
A concrete example:
let's say I want to render a fieldset with a radio element which has 3 options and only one is correct. The Idea is to wrap each option in some div in order to apply bootstrap classes, and then highlight the option (adding has-error class) when (custom) validation fails.
The rendering could be something like this (here directly into the view script):
foreach($form->getFieldsets() as $fieldset) {
echo "<fieldset><legend>".$fieldset->getName()."</legend>";
foreach ($fieldset as $element) {
$opt=$element->getOption('value_options');
foreach ($opt as $key=>$val) {
if($element->getValue()==$key){$check="checked='checked'";}else{$check="";}
echo"<div class='form-group (HERE ADD `has-error` CLASS IF VALIDATION FAILS)'>".
"<label class='custom-control'>".
"<input type='radio' class='custom-control-input' name='".$element->getName()."' value='".$key."' ".$check.">".
"<span class='custom-control-indicator'></span>".
"<span class='custom-control-description'>*TEXT*</span>".
"</label>".
"</div>";
}//3rd foreach
}//2nd foreach
echo "</fieldset>
}//1st foreach
And here is the question: how to detect (after validation) if the element has an error in order to add a bootstrap error class?
I was thinking a possible solution could be:
//custom validator
[...]
public function isValid($value, array $context = null)
{
$this->setValue($value);
if ($value==1) {
$this->error(self::MESSAGE1); //Your answer (radio with value=1) is wrong!
$this->error(self::CODE1); //1
return false;
}
[...]
return true;
}
then:
//updated view script
$messages=$form->getMessages();
foreach($form->getFieldsets() as $fieldset) {
echo "<fieldset><legend>".$fieldset->getName()."</legend>";
foreach ($fieldset as $element) {
$opt=$element->getOption('value_options');
foreach ($opt as $key=>$val) {
if($element->getValue()==$key){$check="checked='checked'";}else{$check=null;}
$field=$fieldset->getName();
$radio=$element->getName();
if(isset($messages[$fieldset][$radio]) && $messages[$fieldset][$radio]==$key){$class='has-error';}else{$class=null;}
echo"<div class='form-group ".$class."'>".
"<label class='custom-control'>".
"<input type='radio' class='custom-control-input' name='".$element->getName()."' value='".$key."' ".$check.">".
"<span class='custom-control-indicator'></span>".
"<span class='custom-control-description'>*TEXT*</span>".
"</label>".
"</div>";
}//3rd foreach
}//2nd foreach
echo "</fieldset>
}//1st foreach
The only problem is that $element->getName()
returns a string with like: "fieldsetName[elementName]"
while $form->getMessages()
returns a multidimensional array like fieldsetName=array(elementName=array(/*MESSAGES*/),);
A possible hack could be to add a custom attribute to the element definition into the form class.
I know that's pretty raw but it's just the concept. What do you think? Any better solution to detect errors?
EDIT:
Probably my first explaination wasn't very clear (my fault). For more clearness, THIS should be the (visual) expected output.
Obviously the idea pretty different since all the logic (filtering/validation/rendering/) should be handled by php/zend. Jquery should only submit the form asynchronously (ajax+serializeArray method) and then return the rendered view (using new viewmodel->setTerminal()). But tht's not the problem.
The problem is that the radio element extends Zend\Form\View\Helper\FormMultiCheckbox, then it's not possible to wrap the single options.
//Module/src/Form/ExampleForm
[...]
$this->add([
'name' => 'radio_element',
'type' => 'radio',
'options' => [
'label' => 'radio_element',
'value_options' => [
1 => 'one',
2 => 'two',
3 => 'three',
],
],
]);
[...]
//the view
echo "<div class='MY_CUSTOM_WRAPPER'>";
echo $this->formRadio($form->get('radio_element'));
echo "</div>";
//Output:
<div class='MY_CUSTOM_WRAPPER'>
<label><input type='radio' name='radio_element' value='1'>one</label>
<label><input type='radio' name='radio_element' value='2'>two</label>
<label><input type='radio' name='radio_element' value='3'>three</label>
</div>
The only way to wrap every single option (as far as I know) is to use a custom helper (In order to keep things simple I actually bypassed this step by acting directly into the view script).
And here is the real problem: since the three options ARE NOT ELEMENTS (but options of an element), I will need to:
- check the selected value and set an error message (custom validator)
- render the element and format the option associated with the error (and not the whole element aka all options).
My solution (updated):
//custom validator
public function isValid($value, array $context = null)
{
$this->setValue($value);
if ($value==1) {
$this->setMessage('Your answer is wrong because...',self::ERRDESC);
$this->error(self::ERRNO); //defined above as %value% -> =1
$this->error(self::ERRDESC);
return false;
}
if ($value==2) {
$this->setMessage('That\'s right!!!',self::ERRDESC);
$this->setMessage("correct".$value,self::ERRNO);
$this->error(self::ERRNO);
$this->error(self::ERRDESC);
return false;
}
if ($value==3) {
$this->setMessage('That\'s really wrong! ',self::ERRDESC);
$this->error(self::ERRNO);//defined above as %value% -> =3
$this->error(self::ERRDESC);
return false;
}
[...]
return true;
}
//view script
foreach($form->getFieldsets() as $fieldset) {
echo "<fieldset><legend>".$fieldset->getName()."</legend>";
foreach ($fieldset as $element) {
$opt=$element->getOption('value_options');
$class2=null;
foreach ($opt as $key=>$val) {
if($element->getValue()==$key){$check="checked='checked'";}else{$check=null;}
$class=null;
if(isset($element->getMessages()['ERRNO'])){
switch($element->getMessages()['ERRNO']){
case $key: $class='is-invalid'; $class2='alert-danger';
break;
case "correct".$key: $class='is-valid'; $class2='alert-success';
break;
}
}
echo"<div class='custom-control custom-radio'>
<input type='radio' id='radio_".$key."' name='".$element->getName()."' class='custom-control-input ".$class."' value='".$key."' ".$check.">
<label class='custom-control-label' for='radio_".$key."'>$val</label>
</div>
";
}//3rd foreach (radio element's options)
echo"<div class='alert $class2' role='alert'>";
echo isset($element->getMessages()['ERRDESC']) ? $element->getMessages()['ERRDESC']:null;
echo "</div>";
}//2nd foreach (radio element)
echo "</fieldset>";
}//1st foreach (fieldset)
Do you know any better solution to get and format the signle radio options?