2

I use the the following code to add a email element to my Zend Framework 2 form:

$form->add(array(
    'type' => 'Zend\Form\Element\Email',
    'name' => 'email',
    'options' => array(
        'label' => 'Email'
    ),
));

By default the getInputSpecification() method of this element sets required to true. But the element object does not contain any required property and consequently the markup does not either.

How could I add markup to my form for my css to be able to add the required suffix? Or at least: how could a custom view helper grab the 'required' setting?

I realise that I could just add a required attribute but that does not feel right as it could possibly get out of sync with the internal element 'required' setting.

tihe
  • 2,452
  • 3
  • 25
  • 27

6 Answers6

3

Although the responses of Sam and Cellulosa are a very nice solution.

I'm worried that these won't be scalable as the ZF2 framework progresses. Even though they are extending the Zend\Form\View\Helper\FormLabel, there is a lot of code in the __invoke method that is duplicate. As changes are made to the framework and the original helper, we would have to constantly be updating your new form view helper to duplicate any changes in the original __invoke method.

A simpler solution would be to call the parent::__invoke() with the provided arguments into the ViewHelper in order to avoid duplicate the code.

So, here is the solution:

Create the ViewHelper at Application/src/Application/Form/View/Helper/RequiredMarkInFormLabel.php

<?php
namespace Application\Form\View\Helper;

use Zend\Form\View\Helper\FormLabel as OriginalFormLabel;
use Zend\Form\ElementInterface;

/**
 * Add mark (*) for all required elements inside a form.
 */
class RequiredMarkInFormLabel extends OriginalFormLabel
{
     /**
     * Invokable
     *
     * @return str
     */    
    public function __invoke(ElementInterface $element = null, $labelContent = null, $position = null)
    {

        // invoke parent and get form label
        $originalformLabel = parent::__invoke($element,$labelContent,$position);

        // check if element is required
        if ($element->hasAttribute('required')) {
            // add a start to required elements
            return '<span class="required-mark">*</span>' . $originalformLabel;
        }else{
            // not start to optional elements
            return  $originalformLabel;
        }
    }
}

Remember to register the ViewHelper at Application/config/module.config.php

'view_helpers' => array(
    'invokables'=> array(
        'formlabel' => 'Application\Form\View\Helper\RequiredMarkInFormLabel'  
    )
),  
Hemerson Varela
  • 24,034
  • 16
  • 68
  • 69
2

You pretty much got it right though. You definitely need your own ViewHelper for that. The easiest way of achieving this is to extend from Zend\Form\View\Helper\FormLabel and overwrite the __invoke() function. This is just a quick try, but i guess this has the potential to work correctly ;)

public function __invoke(ElementInterface $element = null, $labelContent = null, $position = null)
{
    // Implement all default lines of Zend\Form\View\Helper\FormLabel

    // Set $required to a default of true | existing elements required-value
    $required = ($element->hasAttribute('required') ? $element->getAttribute('required') : true);         

    if (true === $required) {
        $labelContent = sprintf(
            '<span class="im-required">(*)</span> %s',
            $labelContent
        );
    }

    return $openTag . $labelContent . $this->closeTag();
}

Remember to register your very own ViewHelper in your Module#getViewHelperConfig() like this:

public function getViewHelperConfig()
{
    return array(
        'factories' => array(
            'myFormLabel' => function($sm) {
                return new Mynamespace\Form\View\Helper\MyFormLabel;
            },
        ),
    );
}

I'm actually kinda surprised such a thing isn't implemented, though I'm sure there's a reason for that :)

Sam
  • 16,435
  • 6
  • 55
  • 89
  • See the last line of my question: by adding a required attribute you have to places to register 'required': one in the form and one in the inputfilter. That does not seem the way to go. – tihe May 16 '13 at 11:08
  • Try to understand the logic behind that. The `attribute[required]` of the form-element represents the HTML-Attribute ``. The `InputFilter` takes care of your business-logic. That's two completely separate concerns, even if they have touch-points. – Sam May 16 '13 at 11:52
  • Validation and markup are indeed two separate concerns. But here we are talking about validation resulting in content. If you separate these you are doing something manually (with the risk of having validation and content out of sync) which can be done automatically. – tihe May 16 '13 at 12:43
1

Using the guidelines provided by Sam, I managed to sort it as follows:

I created a Application/src/Application/Form/View/Helper/RequiredMarkInFormLabel.php file:

<?php
namespace Application\Form\View\Helper;

use Zend\Form\View\Helper\FormLabel as OriginalFormLabel;
use Zend\Form\ElementInterface;
use Zend\Form\Exception;

class RequiredMarkInFormLabel extends OriginalFormLabel
{
    public function __invoke(ElementInterface $element = null, $labelContent = null, $position = null)
    {

        ...

        // Set $required to a default of true | existing elements required-value
        $required = ($element->hasAttribute('required') ? true : false);

        if (true === $required) {
            $labelContent = sprintf(
                '%s<span class="required-mark">*</span>',
                $labelContent
            );
        }

        return $openTag . $labelContent . $this->closeTag();
    }
}

Which I enabled by adding this code in Application/Module.php:

public function getViewHelperConfig()
{
    return array(
        'invokables' => array(
            'formlabel' => 'Application\Form\View\Helper\RequiredMarkInFormLabel',
        ),
    );
}

Hope this will be helpful to somebody! ;)

cellulosa
  • 489
  • 1
  • 6
  • 18
1

What I did was to dynamically add the required atrribute on Elements based on the InputFilter.

On my formHelper:

public function render(Traversable $fieldset)
    {
        ...
        elseif ($element instanceof ElementInterface) {
                if ($fieldset instanceof \Zend\InputFilter\InputFilterProviderInterface){
                    $ifs = $fieldset->getInputFilterSpecification();
                    $start = strrpos($element->getName(), '[')+1;
                    $end = strpos($element->getName(),']');
                    $elementName = substr($element->getName(),$start,$end-$start);
                    if (isset($ifs[$elementName]['required'])){
                        $element->setAttribute('required', $ifs[$elementName]['required']);
                    }

                }
                $form .= $elementHelper->render($element);
        ...

        return $form;
    }

On ElementHelper

public function render(ElementInterface $element){
...
$required = ($element->hasAttribute('required') ? $element->getAttribute('required') : false);
...
if (true === $required && ($label != '' || $label != null || $label != false)) {
     $label .= '*';
}

Thus, you don't have to worry to add attribute and InputSpecification as proposed by @Sam

Josh Crozier
  • 233,099
  • 56
  • 391
  • 304
0

Ok, so I was sick of looking further... therefore i simply parsed the elementname to look for the deepest string between brackets.... here are my viewhelpers to render all elements of a form with the 'form-horizontal' form layout of twitter bootstrap and with an asterisk icon for required fields (required set in the inputfilter not the element attribute)

FormControlGroup.php (ViewHelper to render a form element as a )

<?php

namespace Application\View\Helper;

use Zend\InputFilter\InputFilter;

use Zend\Form\ElementInterface;
use Zend\Form\Element;
use Zend\Form\Fieldset;

use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\View\Helper\FormLabel;
use Zend\Form\View\Helper\FormElement;
use Zend\Form\View\Helper\FormElementErrors;

use Application\View\Helper\FormControlGroupFieldset;

use Zend\Debug\Debug;

class FormControlGroup extends AbstractHelper
{

public function __invoke(ElementInterface $elem = null,InputFilter $inputFilter = null){
    if(!$elem){ return $this; }

    // back up for fieldsets
    if($elem instanceof Fieldset){ 
        if($this->getView()){
            $fcgf = new FormControlGroupFieldset();
            $fcgf->setView($this->getView());
            return $fcgf->__invoke($elem,$inputFilter);
        } else {
            throw new Exception\DomainException(sprintf('FormControlGroup ViewHelper expects either an Element or a Fieldset. No View Could be found in the provided Object.'));
        }
    }

    // add control-group container
    $out = '<div class="control-group">';

    // add control-label class to label if label exists
    $out .= $this->renderLabel($elem,$inputFilter);

    // add controls container
    $out .= '<div class="controls">';

    // ensure renderer for used viewhelpers
    if($this->getView()){
        // render element
        $el = new FormElement();
        $el->setView($this->getView());
        $out .= $el->__invoke($elem);
        unset($el);

        // render element errors
        $er = new FormElementErrors();
        $er->setView($this->getView());
        $out .= $er->__invoke($elem,array('class' => 'help-inline'));
        unset($er);
    } else {
        $out .= 'No renderer found';
    }


    // close containers
    $out .= '</div></div>';
    return $out;
}

/**
 * 
 * @param ElementInterface $elem
 * @return string rendered Label with control-label class
 */
protected function renderLabel(ElementInterface &$elem,InputFilter &$inputFilter = null){
        if($elem->getLabel()){
        $lblAttr = $elem->getLabelAttributes();
        if($lblAttr && key_exists('class', $lblAttr)){
            if(stripos($lblAttr['class'],'control-label')){
                $lblAttr['class'] .= 'control-label';
            }
        } else {
            $lblAttr['class'] = 'control-label';
        }
        $elem->setLabelAttributes($lblAttr);
        // check whether this element is required to fill out
        if($inputFilter){
            $inputs = $inputFilter->getInputs();
            if(key_exists($this->getRealElementName($elem),$inputs)){
                if($inputs[$this->getRealElementName($elem)]->isRequired()){
                    $elem->setLabel($elem->getLabel().'<i class="icon-asterisk"></i>');
                }
            }
        }
        $lbl = new FormLabel();
        return $lbl->__invoke($elem);
    } else {
        return '';
    }
}

public function getRealElementName($e){
    $start = strrpos($e->getName(), '[')+1;
    $end = strpos($e->getName(),']');
    $elName = substr($e->getName(),$start,$end-$start);
    return $elName;
}
}

FormControlGroupFieldset.php (ViewHelper to render a form fieldset as a - invoked by FormControlGroupFieldset)

<?php

namespace Application\View\Helper;


use Zend\InputFilter\InputFilter;

use Zend\Form\Fieldset;
use Zend\Form\View\Helper\AbstractHelper;

use Application\View\Helper\FormControlGroup;

use Zend\Debug\Debug;


class FormControlGroupFieldset extends AbstractHelper
{

public function __invoke(Fieldset $fs=null,InputFilter $inputFilter = null){
    if(!$fs){ return $this; }

    $out = '<fieldset>';

    if($fs->getLabel()){
        $out .= '<legend>'.$fs->getLabel().'</legend>';
    }

    if($this->getView() && sizeof($fs->getElements()) > 0){
        $cg = new FormControlGroup();
        $cg->setView($this->getView());
        if($inputFilter){
            $inputs = $inputFilter->getInputs();
            $fsInput = $inputs[$fs->getName()];
        }
        foreach($fs->getElements() as $e){
            $out .= $cg->__invoke($e,$fsInput);
        }
        unset($cg);
    } else {
        $out .= "No Fieldset renderer found.";
    }

    $out .= '</fieldset>';

    return $out;
}

public function getRealElementName($e){
    $start = strrpos($e->getName(), '[')+1;
    $end = strpos($e->getName(),']');
    $elName = substr($e->getName(),$start,$end-$start);
    return $elName;
}

}

You might need to adjust the namespaces ...

0

For anyone else wondering... There is a easy solution, use css :after pseudo class to append '*' to the label.

label.required:after{
   content:'*';
}

It cannot be simpler than that!

FDIM
  • 1,981
  • 19
  • 21