Skip to content

Instantly share code, notes, and snippets.

@nielsmouthaan
Created September 22, 2012 10:21
Show Gist options
  • Save nielsmouthaan/3765766 to your computer and use it in GitHub Desktop.
Save nielsmouthaan/3765766 to your computer and use it in GitHub Desktop.
Custom KnpMenuBundle navigation bar twig template to support Font Awesome icons & Twitter bootstrap lay-out

For a recent project, I needed to include KnpMenuBundle in a Symfony2 project. This project was styled using the famous Twitter Bootstrap. I’ve found many articles describing how to do this but all of them didn’t exactly resulted in something I wanted: a web application with a navigation bar including icons and dropdown menu’s. In addition, I needed a “active” class indicating which item was visited, including parent/child items.

The files below belong to a tutorial which can be found here: http://nielsmouthaan.nl/symfony2-knpmenubundle-font-awesome-twitter-bootstrap-integration-navigation-bar-including-icons-and-dropdown-menus/

{% extends '::base.html.twig' %}
{% block title %}My Awesome Page!{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/css/bootstrap.no-icons.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/2.0/css/font-awesome.css">
{% endblock %}
{% block body %}
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<a class="brand" href="#">Awesome!</a>
{{ knp_menu_render('AcmeHelloBundle:MenuBuilder:mainMenu', {'currentClass': 'active', 'template': 'AcmeHelloBundle:Menu:knp_menu.html.twig'}) }}
{{ knp_menu_render('AcmeHelloBundle:MenuBuilder:userMenu', {'currentClass': 'active', 'template': 'AcmeHelloBundle:Menu:knp_menu.html.twig'}) }}
</div>
</div>
<div class="content" style="margin-top: 60px;">
Content goes here!
</div>
{% endblock %}
{% block javascripts %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
<script type="text/javascript" src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/js/bootstrap.min.js"></script>
{% endblock %}
{% extends 'knp_menu.html.twig' %}
{% block item %}
{% import "knp_menu.html.twig" as macros %}
{% if item.displayed %}
{%- set attributes = item.attributes %}
{%- set is_dropdown = attributes.dropdown|default(false) %}
{%- set divider_prepend = attributes.divider_prepend|default(false) %}
{%- set divider_append = attributes.divider_append|default(false) %}
{# unset bootstrap specific attributes #}
{%- set attributes = attributes|merge({'dropdown': null, 'divider_prepend': null, 'divider_append': null }) %}
{%- if divider_prepend %}
{{ block('dividerElement') }}
{%- endif %}
{# building the class of the item #}
{%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %}
{%- if matcher.isCurrent(item) %}
{%- set classes = classes|merge([options.currentClass]) %}
{%- elseif matcher.isAncestor(item, options.depth) %}
{%- set classes = classes|merge([options.ancestorClass]) %}
{%- endif %}
{%- if item.actsLikeFirst %}
{%- set classes = classes|merge([options.firstClass]) %}
{%- endif %}
{%- if item.actsLikeLast %}
{%- set classes = classes|merge([options.lastClass]) %}
{%- endif %}
{# building the class of the children #}
{%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %}
{%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %}
{# adding classes for dropdown #}
{%- if is_dropdown %}
{%- set classes = classes|merge(['dropdown']) %}
{%- set childrenClasses = childrenClasses|merge(['dropdown-menu']) %}
{%- endif %}
{# putting classes together #}
{%- if classes is not empty %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- endif %}
{%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %}
{# displaying the item #}
<li{{ macros.attributes(attributes) }}>
{%- if is_dropdown %}
{{ block('dropdownElement') }}
{%- elseif item.uri is not empty and (not item.current or options.currentAsLink) %}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElement') }}
{%- endif %}
{# render the list of children#}
{{ block('list') }}
</li>
{%- if divider_append %}
{{ block('dividerElement') }}
{%- endif %}
{% endif %}
{% endblock %}
{% block dividerElement %}
{% if item.level == 1 %}
<li class="divider-vertical"></li>
{% else %}
<li class="divider"></li>
{% endif %}
{% endblock %}
{% block linkElement %}
<a href="{{ item.uri }}"{{ macros.attributes(item.linkAttributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
</a>
{% endblock %}
{% block spanElement %}
<span>{{ macros.attributes(item.labelAttributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
</span>
{% endblock %}
{% block dropdownElement %}
{%- set classes = item.linkAttribute('class') is not empty ? [item.linkAttribute('class')] : [] %}
{%- set classes = classes|merge(['dropdown-toggle']) %}
{%- set attributes = item.linkAttributes %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- set attributes = attributes|merge({'data-toggle': 'dropdown'}) %}
<a href="#"{{ macros.attributes(attributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
<b class="caret"></b>
</a>
{% endblock %}
{% block label %}{{ item.label|trans }}{% endblock %}
<?php
namespace Acme\HelloBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAware;
class MenuBuilder extends ContainerAware
{
public function mainMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav');
$menu->addChild('Projects', array('route' => 'acme_hello_projects'))
->setAttribute('icon', 'icon-list');
$menu->addChild('Employees', array('route' => 'acme_hello_employees'))
->setAttribute('icon', 'icon-group');
return $menu;
}
public function userMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav pull-right');
/*
You probably want to show user specific information such as the username here. That's possible! Use any of the below methods to do this.
if($this->container->get('security.context')->isGranted(array('ROLE_ADMIN', 'ROLE_USER'))) {} // Check if the visitor has any authenticated roles
$username = $this->container->get('security.context')->getToken()->getUser()->getUsername(); // Get username of the current logged in user
*/
$menu->addChild('User', array('label' => 'Hi visitor'))
->setAttribute('dropdown', true)
->setAttribute('icon', 'icon-user');
$menu['User']->addChild('Edit profile', array('route' => 'acme_hello_profile'))
->setAttribute('icon', 'icon-edit');
return $menu;
}
}
<?php
namespace Acme\HelloBundle\Menu;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class RequestVoter implements VoterInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function matchItem(ItemInterface $item)
{
if ($item->getUri() === $this->container->get('request')->getRequestUri()) {
// URL's completely match
return true;
} else if($item->getUri() !== '/' && (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri())) {
// URL isn't just "/" and the first part of the URL match
return true;
}
return null;
}
}
services:
acme.hello.menu.voter.request:
class: Acme\HelloBundle\Menu\RequestVoter
arguments:
- @service_container
tags:
- { name: knp_menu.voter }
@natelenart
Copy link

Thanks so much for this. I was searching through the knp docs and couldn't figure out how to modify just the template. I'm not sure when you posted this gist, but on dev-master (commit 7baf621fb77867e49daca7dd72b443ea232cb5e1) as of 9/3/2013, I was able to pass { currentClass: 'active', ancestorClass: 'active' } to knp_menu_render to trigger TB active link highlighting on both the current item and its containing dropdown link. This lets me skip dealing with the RequestVoter implementation.

@JulianMBr
Copy link

Maybe some unrelated but you saved me nerves!
I was trying to find a away the see the menu active while browsering in sub-routes like /show/id etc.

Finally I ended up with your code and change a little row for my purpose which I wanted to share:

    public function matchItem(ItemInterface $item)
    {
        if ($item->getUri() === $this->container->get('request')->getRequestUri()) {
            // URL's completely match
            return true;
        } else if($item->getUri() !== $this->container->get('router')->getContext()->getBaseUrl().'/'&& (substr($this->container->get('request')->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri())) {
            // URL isn't just "/" and the first part of the URL match
            return true;
        }
        return null;
    }

So I changed the / into !$this->container->get('router')->getContext()->getBaseUrl().'/ ' adding the baseUrl if not in main directory and adding simply /. So people who acutally have symfony not located in the root ;)

@kabudu
Copy link

kabudu commented Feb 28, 2015

I worked out a way to do this without using a voter or over-riding the ancestorClass, i.e. "active link highlighting on both the current item and its containing dropdown link". See the code snippet below where I am over-riding the linkElement block:

{% block linkElement %}
    {% import 'knp_menu.html.twig' as knp_menu %}
    {% set item_link_attributes = item.linkAttributes %}
    {%- set sub_classes = [] %}
    {%- if matcher.isCurrent(item) %}
        {%- set sub_classes = sub_classes|merge([options.currentClass]) %}
    {%- elseif matcher.isAncestor(item, options.matchingDepth) %}
        {%- set sub_classes = sub_classes|merge(['active']) %}
    {%- endif %}
    {%- if sub_classes is not empty %}
        {%- set item_link_attributes = item_link_attributes|merge({'class': sub_classes|join(' ')}) %}
    {%- endif %}
    <a href="{{ item.uri }}"{{ knp_menu.attributes(item_link_attributes) }}>
        <span>{{ block('label') }}</span>       
    </a>
{% endblock %}

The trick is this bit:

{%- elseif matcher.isAncestor(item, options.matchingDepth) %}
        {%- set sub_classes = sub_classes|merge(['active']) %}

Because the ancestor only gets an ancestor class if one of its children are active, you simply add an 'active' class to the classes for the ancestor link element. Job done! :)

HTH

@cklm
Copy link

cklm commented Feb 19, 2016

Thanks for the work!
If you want to use that with Symfony3 (which doesn't know $this->get('request') anymore) use this gist:
https://gist.github.com/cklm/f71884291249154bf483

@Ishimura413
Copy link

Merci beaucoup ! C'est un bel exemple qui m'a permis de comprendre comment utiliser KnpMenuBundle !

Thx :)

@interso
Copy link

interso commented May 14, 2016

Thanks for your work!
I saw a small mistake. Please, correct file knp_menu.html.twig in line 85.
<span>{{ macros.attributes(item.labelAttributes) }}>

<span {{ macros.attributes(item.labelAttributes) }}>

@steve-todorov
Copy link

I still can't believe I have to do all this digging around just to customize the rendering of a simple menu! Thanks for posting this gist!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment