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=\'bla
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
Zend\Navigation
to benefit from its isActive()
method and the built in translation, ACL, etc support.<li>
element and <a>
element. (ZF2's Menu View Helper supports an 'either or' approach currently)<ul>
elements.<a>
element such as data-*="..."
Solution Description
Zend\View\Helper\Navigation\Menu
renderNormalMenu()
and htmlify()
methodsZend\Pages
to add CSS classes and additional attributes to some elementsSolution
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!
In addition to jmbertucci comment
exсess caret tag in label which cause problem with:
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.
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.