Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ZF2 custom attributes in navigation

How would I add custom attributes into Zend Framework 2 navigation?
I know I can add id or class -> but that's about it....

1) How would I add data-test='blahblah' attribute for example?
2) Can I add attribute to li elements that contain actual links?

$container = new Zend\Navigation\Navigation(array(
    array(
        'label' => 'Page 1',
        'id' => 'home-link',
        'uri' => '/',
    ),
    array(
        'label' => 'Zend',
        'uri' => 'http://www.zend-project.com/',
        'order' => 100,
    ),
);

Edit:

@Bram Gerritsen: Thanks for your answer.

Yes - I can add 'data-test' => 'blahblah' and retrieve it as $page->get('data-test') - but this still doesn't append it as an attribute into <a></a>.... Would I ahve to override htmlify to to that?

like image 925
agent_smith Avatar asked Feb 23 '13 04:02

agent_smith


3 Answers

Bram's answer helped point me to a solution, here's what I needed and how I solved it (since I was new to ZF2 and namespaces it took me much longer than it should have, so hopefully this will help others)

Problem

  • Want to use Zend\Navigation to benefit from its isActive() method and the built in translation, ACL, etc support.
  • Needed to add CSS class name(s) to the <li> element and <a> element. (ZF2's Menu View Helper supports an 'either or' approach currently)
  • Needed to add CSS class name(s) to nested <ul> elements.
  • Needed to add additional attributes to the <a> element such as data-*="..."
  • Needed these changes to support Bootstrap 3 markup

Solution Description

  • Create customer View Helper by extending Zend\View\Helper\Navigation\Menu
  • Slightly modify the renderNormalMenu() and htmlify() methods
  • Take advantage of the ability to add custom properties to Zend\Pages to add CSS classes and additional attributes to some elements

Solution

Step 1

Created custom View Helper under the Application module src\Application\View\Helper\NewMenu.php

NewMenu.php

<?php
namespace Application\View\Helper;

// I'm extending this class, need to include it
use Zend\View\Helper\Navigation\Menu;

// Include namespaces we're using (from Zend\View\Helper\Navigation\Menu)
use RecursiveIteratorIterator;
use Zend\Navigation\AbstractContainer;
use Zend\Navigation\Page\AbstractPage;


class NewMenu extends Menu
{
    // copied fromZend\View\Helper\Navigation\Menu
    protected function renderNormalMenu(...){} 

    // copied from Zend\View\Helper\Navigation\Menu
    public function htmlify(...){}
}

Step 2

Registered new View Helper with the getViewHelperConfig() in \module\Application\Module.php

<?php
/**
 * Zend Framework (http://framework.zend.com/) ...*/

namespace Application;

use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

class Module
{
    // ** snip **

    public function getViewHelperConfig()   {
        return array(
            'invokables' => array(
                // The 'key' is what is used to call the view helper
                'NewMenu' => 'Application\View\Helper\NewMenu',
            )
        );
    }
}

Step 3

In my layout.phtml script, I get my Navigation container and pass it to the NewMenu view helper. I also set some options like adding the parent <ul> class name and not escaping labels so I can add the standard 'dropdown caret' that Bootstrap uses (ie. <b class="caret"></b>) to a label with a dropdown menu.

$container = $this->navigation('navigation')->getContainer();
echo $this->NewMenu($container)->setUlClass('nav navbar-nav')->escapeLabels(false);

Intermission

At this point, we should have more or less just duplicated the Menu View Helper. It should produce a navigation the same way the standard View Helper does.


Step 4

In the NewMenu.php class, I remove the $addClassToListItem code to avoid it from placing classes on the wrong element by accident.

protected function renderNormalMenu(...)

// Add CSS class from page to <li>
//if ($addClassToListItem && $page->getClass()) {
//    $liClasses[] = $page->getClass();
//}

public function htmlify(...)

// Always apply page class to <a> tag. We'll use a diff. method for <li>
//if ($addClassToListItem === false) {
    $attribs['class'] = $page->getClass();
//}

Step 5

Add a method to apply CSS class name to <li> tags, since we removed the $addClassTolistItem method. We simply use the Page classes ability to have custom properties and do this:

protected function renderNormalMenu

// Is page active?
if ($isActive) {
    $liClasses[] = 'active';
}

if($wrapClass = $page->get('wrapClass')){
    $liClasses[] = $wrapClass;
}
...

Now, in our Navigation config file, we can simply add a property called wrapClass to apply CSS classes to the wrapping element (<li>).

config\autoload\global.php

...
'navigation' => array(
    'default' => array(
        ...
        array(
            'label' => 'Products <b class="caret"></b>',
            'route' => 'products',
            'wrapClass' => 'dropdown',         // class to <li>
            'class'     => 'dropdown-toggle',  // class to <a> like usual
            'pages' => array(
                array(
                    'label' => 'Cars',
                    'route' => 'products/type',
                    ...
                ),
                ...
            ),
        ),
...

Step 6

Add the ability to have additional attributes on <a> like data-*. For Bootstrap 3 you'll need data-toggle="dropdown" for example.

public function htmlify(...)

// get attribs for element
$attribs = array(
    'id'     => $page->getId(),
    'title'  => $title,
);

// add additional attributes
$attr = $page->get('attribs');
if(is_array($attr)){
    $attribs = $attribs + $attr;
}

In your config file, you can now add a property with an array of additional attributes:

config\autoload\global.php

...
'navigation' => array(
    'default' => array(
        ...
        array(
            'label' => 'Products <b class="caret"></b>',
            'route' => 'products',
            'wrapClass' => 'dropdown',         // class to <li>
            'class'     => 'dropdown-toggle',  // class to <a> like usual

            'attribs'   => array(
                'data-toggle' => 'dropdown',  // Key = Attr name, Value = Attr Value
            ),

            'pages' => array(
                array(
                    'label' => 'Cars',
                    'route' => 'products/type',
                    ...
                ),
                ...
            ),
        ),
...

Step 7

Add the ability to place class names on nested lists container (ie. <ul>).

protected function renderNormalMenu()

if ($depth > $prevDepth) {
    // start new ul tag
    if ($ulClass && $depth ==  0) {
        $ulClass = ' class="' . $ulClass . '"';
    }

    // Added ElseIf below

    else if($ulClass = $page->get('pagesContainerClass')){
        $ulClass = ' class="' . $ulClass . '"';
    }

    else {
        $ulClass = '';
    }
    $html .= $myIndent . '<ul' . $ulClass . '>' . self::EOL;

The original code basically said "if this is the first <ul> and there's a UL class, add it, else do nothing. So, I added an additional check to say, if a property called pagesContainerClass is available, to apply the class to the <ul> as well.

This means we need to add the property on the right Page in our configuration:

config\autoload\global.php

...
'navigation' => array(
    'default' => array(
        ...
        array(
            'label' => 'Products <b class="caret"></b>',
            'route' => 'products',
            'wrapClass' => 'dropdown',         // class to <li>
            'class'     => 'dropdown-toggle',  // class to <a> like usual

            'attribs'   => array(
                'data-toggle' => 'dropdown',  // Key = Attr name, Value = Attr Value
            ),

            'pages' => array(
                array(
                    'label' => 'Cars',
                    'route' => 'products/type',
                    // Give child <ul> a class name
                    'pagesContainerClass' => 'dropdown-menu',
                    ...
                ),
                ...
            ),
        ),
...

Important to note, the UL class needs to be placed on the first child Page of a child because the conditional statements are wrapped in a the following condition:

if ($depth > $prevDepth) {
    // start new ul tag
    ...
}

After the first child is called, the $dept = $prevDepth and the nested <ul> will have already been sent to the string buffer.


This solution hasn't been rigorously tested but the idea is that is simply takes the current Menu View Helper, and overloads the two necessary methods and only slightly modifies that.

I've tried to use setPartial() but that only helped with the <li> generation, it was still using the Menu View Helpers' htmlify() method (all of which was mentioned in Bram's discussion above).

So with making those small tweeks to the to methods and using the Page class's ability to have custom properties, I could just add some additional logic to get class names on the <li>, <a> and nested <ul> classes as well as add additional properties on the <a> elements, so I could configure my Zend\Navigation from the config to spit out, basically, Bootstrap 3 Navbar markup.

The end Layout then just looks like this:

<nav class="navbar navbar-default navbar-static-top" role="navigation">
    <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
        </button>
    </div>
    <div class="collapse navbar-collapse navbar-ex1-collapse">
    <?php
        // Use Zend\Navigation to create the menu
        $container = $this->navigation('navigation')->getContainer();
        echo $this->NewMenu($container)->setUlClass('nav navbar-nav')->escapeLabels(false);
    ?>
    </div><!-- /.navbar-collapse -->
</nav>

The troubles I kept running into was a better understanding of PHP Namespaces and having needed to include the appropriate Qualified namespaces in my custom View Helper, even though I was extending it.

The other problem, was that the Navigation View Helper can call the Menu View Helper from itself like so:

$this->navigation('navigation')->menu();

This won't work:

$this->navigation('navigation')->NewMenu();

I'm thinking because of namespace issues with NewMenu not being registered in the Navigation View Helper class and I'm not going to extend it just for that.

So, hopefully this (long) answer will help others who are struggling with this need.

Cheers!

like image 67
jmbertucci Avatar answered Nov 15 '22 12:11

jmbertucci


The Page classes have some dedicated setters for common attributes (setLabel, setId, setUri etc), If a setter not exists __set will be called. See the manual for more information about this and also about extending the AbstractPage class.

array(
    'label' => 'Page 1',
    'id' => 'home-link',
    'uri' => '/',
    'data-test' => 'blahblah'
),

Now you can do $page->get('data_test') and it will return blahblah.

Your second question is about altering the rendering of the menu (adding a attribute to the li. ZF2 is using the menu view helper to render a navigation menu. All the navigation view helpers have an option to use your own partial view for rendering using setPartial().

In your viewscript:

$partial = array('menu.phtml', 'default');
$this->navigation()->menu()->setPartial($partial);
echo $this->navigation()->menu()->render();

In your partial view menu.phtml do something like this:

<ul>
<?php foreach ($this->container as $page): ?>
    <li data-test="<?=$page->get('data_test')?>"><?=$this->navigation()->menu()->htmlify($page)?></li>
<?php endforeach; ?>
<ul>

This will only render the highest level of the menu. If you have deeper/nested structure your custom view script will end up far more complex.

Hope this helps.

like image 41
Bram Gerritsen Avatar answered Nov 15 '22 12:11

Bram Gerritsen


In addition to jmbertucci comment

Problem

exсess caret tag in label which cause problem with:

  • breadcrumbs
  • menu translation

Splution

To prevent adding tag caret to label you can add support of this parameter at menu config. You should

Go to

src\Application\View\Helper\NewMenu.php

protected function renderNormalMenu()

/// add 4th parameter $page->get('caret')
$html .= $myIndent . '    <li' . $liClass . '>' . PHP_EOL .
$myIndent . '        ' .
$this->htmlify($page, $escapeLabels, $addClassToListItem, $page->get('caret')) . PHP_EOL;

public function htmlify()

} else {
    $html .= $label;
}
//// add this if
if($caret === true){
    $html .= '<b class="caret"></b>';
}

$html .= '</' . $element . '>';

Now you can use it:

 array(
                'label' => 'Some label',
                'caret' => true,
                'route' => 'someroute',
                'wrapClass' => 'dropdown',
                'class' => 'dropdown-toggle',

ps . jmbertucci, you are the Man.

like image 1
gurt Avatar answered Nov 15 '22 12:11

gurt