-
-
Save nateevans/9958390 to your computer and use it in GitHub Desktop.
{% extends '::base.html.twig' %} | |
{% block title %}My Awesome Page!{% endblock %} | |
{% block stylesheets %} | |
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"> | |
{% endblock %} | |
{% block body %} | |
<div class="navbar navbar-default navbar-fixed-top"> | |
<div class="container-fluid"> | |
{# Brand and toggle get grouped for better mobile display #} | |
<div class="navbar-header"> | |
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#admin-navbar-collapse-1"> | |
<span class="sr-only">Toggle navigation</span> | |
<span class="icon-bar"></span> | |
<span class="icon-bar"></span> | |
<span class="icon-bar"></span> | |
</button> | |
<a class="navbar-brand" href="#">Awesome!</a> | |
</div> | |
{# Collect the nav links, forms, and other content for toggling #} | |
<div class="collapse navbar-collapse" id="admin-navbar-collapse-1"> | |
{{ 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> | |
<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/bootstrap/3.1.1/js/bootstrap.min.js"></script> | |
{% endblock %} |
<?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() !== $this->container->get('request')->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; | |
} | |
} |
services: | |
acme.hello.menu.voter.request: | |
class: Acme\HelloBundle\Menu\RequestVoter | |
arguments: | |
- @service_container | |
tags: | |
- { name: knp_menu.voter } |
Genius I'll give this a go later. Spent an hour trying to mod the knp_menu.twig last night to accomplish this but didn't get too far, hah.
Thanks @nateevans !
IMHO the "label" (as described http://symfony.com/doc/master/bundles/KnpMenuBundle/i18n.html)
should be:
{%- block label %}
{{ item.label|trans(
item.getExtra('translation_params', {}),
item.getExtra('translation_domain', 'messages')
) }}
{%- endblock %}
Thanks for this (and Niels for the original post). Here is an updated RequestVoter passing the RequestStack instead of the whole container:
<?php
namespace Acme\HelloBundle\Menu;
use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class RequestVoter implements VoterInterface
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function matchItem(ItemInterface $item)
{
$request = $this->requestStack->getCurrentRequest();
if ($item->getUri() === $request->getRequestUri()) {
// URL's completely match
return true;
} else if ($item->getUri() !== $request->getBaseUrl() . '/'
&& substr($request->getRequestUri(), 0, strlen($item->getUri())) === $item->getUri()) {
// URL isn't just "/" and the first part of the URL match
return true;
}
return null;
}
}
And the updated service file:
services:
acme.hello.menu.voter.request:
class: Acme\HelloBundle\Menu\RequestVoter
arguments: [ @request_stack ]
tags:
- { name: knp_menu.voter }
Why should I add the RequestVoter? Everything works fine without it?
I don't understand the reason to add the RequestVoter.
Thanks @nateevans
Just a little correction, the span element in knp_menu.html.twig is closed before the macros argument
<span>{{ macros.attributes(item.labelAttributes) }}>
it should be:
<span {{ macros.attributes(item.labelAttributes) }}>
Great work!
how do you use the divider element ?
Hi. New here.
I'm trying to use this example.
But I get an error that says "Unable to find template "AppBundle:Menu:knp_menu.html.twig""
I create the knp_menu.html.twig inside the views folder in the App/Resources folder.
This is the part I dont understand:
{{ knp_menu_render('AppBundle:MenuBuilder:mainMenu', {'currentClass': 'active', 'template': 'AppBundle:Menu:knp_menu.html.twig'}) }}
Does the second parameter with the twig has to be under a menu folder inside the AppBundle???
Thanks in advance
Hi there,
Thank you for these snippets. I have created a derivative of it to let it work with KeenThemes Metronic Admin Template 4 (sidebar): https://gist.github.com/woutsanders/79b3551f2c4f07ddaab3c74fa9aba132
I'm sure some stuff could be done a bit more efficient, but it's my first experience with this bundle and thus things could be a bit messy here and there.
I couldn't have done it without this gist (as mentioned in my gist).
Again, thank you for the work Niels! And Nate for posting it here :-)
I fixed several bugs in the template (and made some corresponding modification in builder):
https://gist.github.com/Invis1ble/503db8bb9ff6dafc52c8b2bf7c6fec42
@JHGitty, the same question.
Why we need request voter ?
just curious, but the extends line in knp_menu.html.twig refers back to itself so this ends up in an endless loop. Am I missing something? I put the twig file in my Resources/views folder however I feel like that's probably not where I am supposed to put it...
Anyone got a solution for Bootstrap 4?
Bootstrap 4
Just be aware that dropdowns my bootstrap 4, is not officially supported
As multiple level is not currently supported by bootstrap 4
This requires you to install
https://github.com/bootstrapthemesco/bootstrap-4-multi-dropdown-navbar
And set the the use_multilevel = true
{% extends 'knp_menu.html.twig' %}
{% macro setCssClassAttribute(item, type, add) %}
{% set getter = 'get' ~ type %}
{% set setter = 'set' ~ type %}
{% set value = attribute(item, getter, ['class']) %}
{% if value is iterable %}
{% set value = value|join(' ') %}
{% endif %}
{% do attribute(item, setter, ['class', value ~ ' ' ~ add]) %}
{% endmacro %}
{% block item %}
{% import "knp_menu.html.twig" as macros %}
{#
As multiple level is not currently supported by bootstrap 4
This requires you to install
https://github.com/bootstrapthemesco/bootstrap-4-multi-dropdown-navbar
And set the the use_multilevel = true
#}
{% set use_multilevel = false %}
{% 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'), 'nav-item'] : ['nav-item'] %}
{%- 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(' ') }) %}
<li{{ macros.attributes(attributes) }}>
{# displaying the item #}
{%- if is_dropdown %}
{{ block('dropdownElement') }}
{%- elseif item.uri is not empty and (not item.current or options.currentAsLink) %}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElement') }}
{%- endif %}
{%- if divider_append %}
{{ block('dividerElement') }}
{%- endif %}
{% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
{{ block('dropdownlinks') }}
{% endif %}
</li>
{% endif %}
{% endblock %}
{% block dropdownlinks %}
{% if use_multilevel %}
<ul class="dropdown-menu">
{% else %}
<div class="dropdown-menu">
{% endif %}
{% for item in item.children %}
{{ block('renderDropdownlink') }}
{% if use_multilevel and item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
{{ block('dropdownlinks') }}
{% endif %}
{% endfor %}
{% if not use_multilevel %}
</div>
{% else %}
</ul>
{% endif %}
{% endblock %}
{% block renderDropdownlink %}
{% import _self as ownmacro %}
{%- set divider_prepend = item.attributes.divider_prepend|default(false) %}
{%- set divider_append = item.attributes.divider_append|default(false) %}
{%- set attributes = item.attributes|merge({'dropdown': null, 'divider_prepend': null, 'divider_append': null }) %}
{% if use_multilevel %}
<li>
{% endif %}
{%- if divider_prepend %}
{{ block('dividerElementDropdown') }}
{%- endif %}
{%- if item.uri is not empty and (not item.current or options.currentAsLink) %}
{{ ownmacro.setCssClassAttribute(item, 'LinkAttribute', 'dropdown-item') }}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElementDropdown') }}
{%- endif %}
{%- if divider_append %}
{{ block('dividerElementDropdown') }}
{%- endif %}
{% if use_multilevel %}
</li>
{% endif %}
{% endblock %}
{% block spanElementDropdown %}
{% import "knp_menu.html.twig" as macros %}
{% import _self as ownmacro %}
{{ ownmacro.setCssClassAttribute(item, 'LabelAttribute', 'dropdown-header') }}
<div {{ macros.attributes(item.labelAttributes) }}>
{% if item.attribute('icon') is not empty %}
<i class="{{ item.attribute('icon') }}"></i>
{% endif %}
{{ block('label') }}
</div>
{% endblock %}
{% block dividerElementDropdown %}
<div class="dropdown-divider"></div>
{% endblock %}
{% block dividerElement %}
{% if item.level == 1 %}
<li class="divider-vertical"></li>
{% else %}
<li class="divider"></li>
{% endif %}
{% endblock %}
{% block linkElement %}
{% import "knp_menu.html.twig" as macros %}
{% import _self as ownmacro %}
{{ ownmacro.setCssClassAttribute(item, 'LinkAttribute', 'nav-link') }}
<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 %}
{% import "knp_menu.html.twig" as macros %}
{% import _self as ownmacro %}
{{ ownmacro.setCssClassAttribute(item, 'LabelAttribute', 'navbar-text') }}
<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 %}
{% import "knp_menu.html.twig" as macros %}
{%- set classes = item.linkAttribute('class') is not empty ? [item.linkAttribute('class')] : [] %}
{%- set classes = classes|merge(['dropdown-toggle', 'nav-link']) %}
{%- 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 %}
Works with the following
$menu = $event->getItem();
$menu->addChild(
'linking',
[
'route' => 'profile_index',
]
);
$menu->addChild(
'texting',
[
'labelAttributes' => [
'class' => 'class3 class4',
],
]
);
$dropdown = $menu->addChild(
'Hello Me',
[
'attributes' => [
'dropdown' => true,
],
]
);
$dropdown->addChild(
'Profile',
[
'route' => 'profile_index',
'attributes' => [
'divider_append' => true,
],
]
);
$dropdown->addChild(
'text',
[
'attributes' => [
'icon' => 'fa fa-user-circle',
],
'labelAttributes' => [
'class' => ['class1', 'class2'],
],
]
);
$dropdown->addChild(
'Logout',
[
'route' => 'logout',
'attributes' => [
'divider_prepend' => true,
'icon' => 'fa fa-sign-out',
],
]
);
@nateevans Why is the RequestVoter
required (it works without it)?
Got error: Accessing Twig_Template attributes is forbidden.
what is $event
variable?
Thanks @nateevans !