Skip to content

Instantly share code, notes, and snippets.

@lsv
Last active May 4, 2024 17:05
Show Gist options
  • Save lsv/24a9e9d7d961d22226f8e220d2c4a7d0 to your computer and use it in GitHub Desktop.
Save lsv/24a9e9d7d961d22226f8e220d2c4a7d0 to your computer and use it in GitHub Desktop.
KNP Menu Bundle - Bootstrap 4 and Font Awesome 4

Most of the hard work is done by @nateevans https://gist.github.com/nateevans/9958390

Thank you!

Requires Twig ^1.2 | ^2.0

{% 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 %}

Tested it with

$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',
        ],
    ]
);
  • NOT tested fully with multilevel dropdowns (as its not official supported, and will not be supported)
  • NOT tested with FontAwesome 5 (though, it should just be to add the correct icon classes)
@K41R1
Copy link

K41R1 commented Sep 2, 2018

@tacman
Copy link

tacman commented Jun 18, 2019

Does the call to knp_menu_render need to be surrounded by a div? If so, what classes?

        <div class="collapse navbar-collapse" id="navbarResponsive">
        {{ knp_menu_render('test_menu', {'currentClass': 'active', 'template': 'knp_menu.html.twig'}) }}
        </div>

Also, 'class' => ['class1', 'class2'], is failing for me, fixed with 'class' => 'class1 class2'

@Fbeen
Copy link

Fbeen commented Nov 10, 2019

I had an issue that the links in the dropdown still had the css-class "nav-link" which gave some margin problems. To solve my problem i made a if statement inside the linkElement block. Hope that i can make somebody happy ;-)

{% block linkElement %}
    {% import "knp_menu.html.twig" as macros %}
    {% import _self as ownmacro %}

    {# Prevent adding nav-link class if class dropdown-item has been added #}
    {% if not (item.linkAttributes.class is defined and 'dropdown-item' in item.linkAttributes.class) %}
        {{ ownmacro.setCssClassAttribute(item, 'LinkAttribute', 'nav-link') }}
    {% endif %}
    
    <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 %}

@tacman
Copy link

tacman commented Nov 11, 2019

I call

  return $this->cleanupMenu($menu)

in every MenuBuilder method. It sure feels like an ugly hack, but it appears to be working (after hours of gnashing of teeth and hair-pulling).

public function cleanupMenu(ItemInterface $menu): ItemInterface
{

    $menu->setChildrenAttribute('class', 'navbar-nav');
    // menu items
    foreach ($menu as $child) {
        $child->setLinkAttribute('class', 'nav-link')
            ->setAttribute('class', 'nav-item');
    }
    return $menu;
}

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