Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zend_Framework Decorators Wrap Label and ViewHelper inside a div

I am new to this, zend decoration malarchy, but i have two significant questions that i cant get my head around. Question one is followed by some example

$decorate = array(
    array('ViewHelper'),
    array('Description'),
    array('Errors', array('class'=>'error')),
    array('Label', array('tag'=>'div', 'separator'=>' ')),
    array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);

...

$name = new Zend_Form_Element_Text('title');
$name->setLabel('Title')
    ->setDescription("No --- way");

$name->setDecorator($decorate);

Which outputs

<li class="element">
    <label for="title" class="required">Title</label> 
    <input type="text" name="title" id="title" value="">
    <p class="hint">No --- way</p>
    <ul class="error">
        <li>Value is required and can't be empty</li>
    </ul>
</li>

The Question #1

How do i wrap the label and the input around a div tag? So the output is as follows:

<li class="element">
    <div>
        <label for="title" class="required">Title</label> 
        <input type="text" name="title" id="title" value="">
    </div>
    <p class="hint">No --- way</p>
    <ul class="error">
        <li>Value is required and can't be empty</li>
    </ul>
</li>

The Question #2

What is up with the order of the elements in the $decorate array? They MAKE NO SENSE!

like image 236
Angel.King.47 Avatar asked Sep 25 '11 14:09

Angel.King.47


2 Answers

The decorator pattern is a design pattern for adding functionality to existing classes without altering those existing classes. In stead, a decorator class wraps itself around another class, and generally exposes the same interface as the decorated class.

Basic example:

interface Renderable
{
    public function render();
}

class HelloWorld
    implements Renderable
{
    public function render()
    {
        return 'Hello world!';
    }
}

class BoldDecorator
    implements Renderable
{
    protected $_decoratee;

    public function __construct( Renderable $decoratee )
    {
        $this->_decoratee = $decoratee;
    }

    public function render()
    {
        return '<b>' . $this->_decoratee->render() . '</b>';
    }
}

// wrapping (decorating) HelloWorld in a BoldDecorator
$decorator = new BoldDecorator( new HelloWorld() );
echo $decorator->render();

// will output
<b>Hello world!</b>

Now, you might be tempted to think that because the Zend_Form_Decorator_* classes are decorators, and have a render method, this automatically means the output of the decorated class' render method will always be wrapped with additional content by the decorator. But on inspection of our basic example above, we can easily see this doesn't necessarily have to be the case at all of course, as illustrated by this additional (albeit fairly useless) example:

class DivDecorator
    implements Renderable
{
    const PREPEND = 'prepend';
    const APPEND  = 'append';
    const WRAP    = 'wrap';

    protected $_placement;

    protected $_decoratee;

    public function __construct( Renderable $decoratee, $placement = self::WRAP )
    {
        $this->_decoratee = $decoratee;
        $this->_placement = $placement;
    }

    public function render()
    {
        $content = $this->_decoratee->render();
        switch( $this->_placement )
        {
            case self::PREPEND:
                $content = '<div></div>' . $content;
                break;
            case self::APPEND:
                $content = $content . '<div></div>';
                break;
            case self::WRAP:
            default:
                $content = '<div>' . $content . '</div>';
        }

        return $content;
    }
}

// wrapping (decorating) HelloWorld in a BoldDecorator and a DivDecorator (with DivDecorator::APPEND)
$decorator = new DivDecorator( new BoldDecorator( new HelloWorld() ), DivDecorator::APPEND );
echo $decorator->render();

// will output
<b>Hello world!</b><div></div>

This is in fact basically how a lot of Zend_Form_Decorator_* decorators work, if it makes sense for them to have this placement functionality.

For decorators where it makes sense, you can control the placement with the setOption( 'placement', 'append' ) for instance, or by passing the option 'placement' => 'append' to the options array, for instance.

For Zend_Form_Decorator_PrepareElements, for instance, this placement option is useless and therefor ignored, as it prepares form elements to be used by a ViewScript decorator, making it one of the decorators that doesn't touch the rendered content of the decorated element.

Depending on the default functionality of the individual decorators, either the content of the decorated class is wrapped, appended, prepended, discarded or something completely different is done to the decorated class, without adding something directly to the content, before passing along the content to the next decorator. Consider this simple example:

class ErrorClassDecorator
    implements Renderable
{
    protected $_decoratee;

    public function __construct( Renderable $decoratee )
    {
        $this->_decoratee = $decoratee;
    }

    public function render()
    {
        // imagine the following two fictional methods
        if( $this->_decoratee->hasErrors() )
        {
            $this->_decoratee->setAttribute( 'class', 'errors' );
        }

        // we didn't touch the rendered content, we just set the css class to 'errors' above
        return $this->_decoratee->render();
    }
}

// wrapping (decorating) HelloWorld in a BoldDecorator and an ErrorClassDecorator
$decorator = new ErrorClassDecorator( new BoldDecorator( new HelloWorld() ) );
echo $decorator->render();

// might output something like
<b class="errors">Hello world!</b>

Now, when you set the decorators for a Zend_Form_Element_* element, they will be wrapped, and consequently executed, in the order in which they are added. So, going by your example:

$decorate = array(
    array('ViewHelper'),
    array('Description'),
    array('Errors', array('class'=>'error')),
    array('Label', array('tag'=>'div', 'separator'=>' ')),
    array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);

... basically what happens is the following (actual class names truncated for brevity):

$decorator = new HtmlTag( new Label( new Errors( new Description( new ViewHelper() ) ) ) );
echo $decorator->render();

So, on examining your example output, we should be able to distill the default placement behaviour of the individual decorators:

// ViewHelper->render()
<input type="text" name="title" id="title" value="">

// Description->render()
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p> // placement: append

// Errors->render()
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p>
<ul class="error"> // placement: append
    <li>Value is required and cant be empty</li>
</ul>

// Label->render()
<label for="title" class="required">Title</label> // placement: prepend
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p>
<ul class="error">
    <li>Value is required and cant be empty</li>
</ul>

// HtmlTag->render()
<li class="element"> // placement: wrap
    <label for="title" class="required">Title</label>
    <input type="text" name="title" id="title" value="">
    <p class="hint">No --- way</p>
    <ul class="error">
        <li>Value is required and cant be empty</li>
    </ul>
</li>

And what do you know; this actually is the default placement of all respective decorators.

But now comes the hard part, what do we need to do to get the result you are looking for? In order to wrap the label and input we can't simply do this:

$decorate = array(
    array('ViewHelper'),
    array('Description'),
    array('Errors', array('class'=>'error')),
    array('Label', array('tag'=>'div', 'separator'=>' ')),
    array('HtmlTag', array('tag' => 'div')), // default placement: wrap
    array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);

... as this will wrap all preceding content (ViewHelper, Description, Errors and Label) with a div, right? Not even... the added decorator will be replaced by the next one, as decorators are replaced by a following decorator if it is of the same class. In stead you would have to give it a unique key:

$decorate = array(
    array('ViewHelper'),
    array('Description'),
    array('Errors', array('class'=>'error')),
    array('Label', array('tag'=>'div', 'separator'=>' ')),
    array(array('divWrapper' => 'HtmlTag'), array('tag' => 'div')), // we'll call it divWrapper
    array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);

Now, we're still faced with the problem that divWrapper will wrap all preceding content (ViewHelper, Description, Errors and Label). So we need to be creative here. There's numerous ways to achieve what we want. I'll give one example, that probably is the easiest:

$decorate = array(
    array('ViewHelper'),
    array('Label', array('tag'=>'div', 'separator'=>' ')), // default placement: prepend
    array(array('divWrapper' => 'HtmlTag'), array('tag' => 'div')), // default placement: wrap
    array('Description'), // default placement: append
    array('Errors', array('class'=>'error')), // default placement: append
    array('HtmlTag', array('tag' => 'li', 'class'=>'element')), // default placement: wrap
);

For more explanation about Zend_Form decorators I'd recommend reading Zend Framework's lead developer Matthew Weier O'Phinney's article about Zend Form Decorators

like image 55
Decent Dabbler Avatar answered Nov 04 '22 11:11

Decent Dabbler


Question #1

Change the decorators order and add an HtmlTag helper this way :

$decorate = array(
    array('ViewHelper'),
    array('Label', array('tag'=>'div', 'separator'=>' ')),
    array('HtmlTag', array('tag' => 'div')),
    array('Description'),
    array('Errors', array('class'=>'error')),
    array('HtmlTag', array('tag' => 'li', 'class'=>'element'))
);

Question #2

Decorators are a chain, the output of each one is passed to the input of the next one, in order to be 'decorated' by it.

By default, they append content (description, errors), prepend content (label..) and or wrap something around (HtmlTag). But these are default behaviors, and you can change it for most of them :

array('HtmlTag', array('tag' => 'span', placement=>'APPEND'));
//this would append <span></span> to the output of the previous decorator instead of wrapping it inside the <span>

Let's have a more closer look to what happens in your chain :

  1. ViewHelper renders your form element using it's default viewHelper, declared in the form element's class.

  2. Label prepends the label to the previous output

  3. HtmlTag wraps a <div> around

  4. Description appends the elements description

  5. Errors appends error messages, if any

  6. HtmlTag wraps all this in an <li>

EDIT

I wrote this answer without testing anything, so there might be some little inaccuracies here and there. Dear reader, if you see some just drop a comment and i'll update.

like image 2
Frederik Eychenié Avatar answered Nov 04 '22 10:11

Frederik Eychenié