Mezzanine Menus with Materialize CSS
Let’s create nested menus in Mezzanine using Materialize CSS.
With the approach below, you get this results. The menus are nested only one level, but with a bit of tweaking, you could get extra levels, provided your framework supports multiple nested menus.
I don’t need more than one level at the moment, thus this:
https://youtu.be/nkcfDKq-Rjc
For some time, I’ve been trying to understand how to create hierarchical menus with Mezzanine. I’m still new, and the learning curve is very steep, with almost not-satisfying documentations.
I hope this might help you get a better understanding of Mezza’s-Menus
So, let’s get going:
Assuming you have your primary.html file ready, and via {% includes ‘pages/menus/primary’ %} you’ve included into your base.html, my primary.html looks like this:
{% load pages_tags i18n mezzanine_tags %} {% if page_branch_in_menu %} {% if branch_level == 0 %} <a href="#" data-activates="mobile-demo" class="button-collapse"><i class="material-icons">menu</i></a> <ul class="right hide-on-med-and-down"> <li> <a class="modal-trigger" href="#searchbox"><i class="material-icons">search</i></a></li> {% if user.is_authenticated %} <li> <a href="{% url 'add_project' %}"><i class="material-icons">add</i></a></li> {% endif %} {% for page in page_branch %} {% if not has_home and page.is_primary and forloop.first %} <li id="primary-menu-home" class="first{% if on_home %} active{% endif %}"> <a href="{% url "home" %}">{% trans "Home" %}</a> </li> {% endif %} {% if page.in_menu %} <li id="primary-menu-{{ page.html_id }}" class="{% if page.is_current_or_ascendant %}active{% endif %}{% if forloop.last %} last{% endif %}"> <a href="{{ page.get_absolute_url }}" {% if page.has_children_in_menu %} class='dropdown-button' data-activates='testmenu{{ page.html_id}}' {% endif %} >{{ page.title }}</a> {% if page.has_children_in_menu %} <div id='testmenu{{page.html_id}}' class='dropdown-content'> {% page_menu page %} </div> {% endif %} </li> {% endif %} {% endfor %} {% ifinstalled mezzanine.blog %} <li> <a href="{% url 'blog_post_list' %}"><i class="material-icons">import_contacts</i></a></li> {% endifinstalled %} {% if user.is_authenticated %} <li> <a class='dropdown-button' href='#' data-activates='myaccount'><i class="material-icons">account_circle</i></a> </li> {% else %} <li> <a href="{% url 'account_profile' %}"><i class="material-icons">account_circle</i></a></li> {% endif %} </ul> {% else %} <a href="#" data-activates="mobile-demo" class="button-collapse"><i class="material-icons">menu</i></a> <ul class="right hide-on-med-and-down"> {% for page in page_branch %} {% if not has_home and page.is_primary and forloop.first %} <li id="primary-menu-home" class="first{% if on_home %} active{% endif %}"> <a href="{% url "home" %}">{% trans "Home" %}</a> </li> {% endif %} {% if page.in_menu %} <li id="primary-menu-{{ page.html_id }}" class="{% if page.is_current_or_ascendant %}active{% endif %}{% if forloop.last %} last{% endif %}"> <a href="{{ page.get_absolute_url }}">{{ page.title }}</a> </li> {% endif %} {% endfor %} <li class="divider-vertical"></li> </ul> {% endif %} {% endif %}
Might be a lot to take in, but you will understand better when you figure out how the {% page_menu %} tag works, which comes built-in with mezzanine. It does most of the heavy lifting, allowing you to concentrate on getting your menus ready.
It does most of the heavy lifting, allowing you to concentrate on getting your menus ready.
How {% page_menu %} works
Rest assured, the {% page_menu %} works, with a bit of mysterious magical ways to it. But here’s how I came to understand its behavior (I stand to be corrected):
{% page_menu %} accepts two arguments, from Mezza docs:
“The
page_menu
template tag is responsible for rendering a single branch of the page tree at a time, and accepts two optional arguments.The arguments are the name of a menu template to use for a single branch within the page tree, and the parent menu item for the branch that will be rendered.”
Let’s go over the code snippet piece by piece, and understand what’s going on:
{% load pages_tags i18n mezzanine_tags %} {% if page_branch_in_menu %} {% if branch_level == 0 %} .... {% else %} ..... {% endif %} {% endif %}
You need to load the ‘pages_tags’ if you wanna use the {% page_menu %}. If you’ll use any other tags in your menus, load them accordingly.
Next, we’re simply making use of the many booleans the {% page_menu %} tag has. So many booleans.
So, we want to know, does my ‘page’ have branches or submenus? If it does, then we’re in to do some branching.
The next part checks the level on the branch on which we are. branch == 0 if true, means we’re looking at the top of the navigation tree, that is, the primary navigation.
So we might have our primary branch to consist of ‘Home’, ‘About’, ‘Contact’. Then under ‘About’, we might have, ‘Team’, ‘Mission’, ‘Case Studies’. With that instance in mind, ‘Home’ and ‘About’ will be on branch 0, then ‘Team’, ‘Mission’ and ‘Case Studies’ will be in branch 1, and so forth.
We’ve confirmed there are submenus in our tree. We’ve confirmed we’re on top of the tree. Then:
{% for page in page_branch %} {% if not has_home and page.is_primary and forloop.first %} <li id="primary-menu-home" class="first{% if on_home %} active{% endif %}"> <a href="{% url "home" %}">{% trans "Home" %}</a> </li> {% endif %} {% if page.in_menu %} <li id="primary-menu-{{ page.html_id }}" class="{% if page.is_current_or_ascendant %}active{% endif %}{% if forloop.last %} last{% endif %}"> <a href="{{ page.get_absolute_url }}" {% if page.has_children_in_menu %} class='dropdown-button' data-activates='testmenu{{ page.html_id}}' {% endif %} >{{ page.title }}</a> {% if page.has_children_in_menu %} <div id='testmenu{{page.html_id}}' class='dropdown-content'> {% page_menu page %} </div> {% endif %} </li> {% endif %} {% endfor %}
Might seem lots are happening here, but it’s simple:
- The page_branch is iterable of ‘Pages’, where we can run through, and get the individual pages from it. We’ll have access to the page.get_absolute_url method and page.title giving us the link to the page model in context and the title respectively.
{% if not has_home and page.is_primary and forloop.first %} <li id="primary-menu-home" class="first{% if on_home %} active{% endif %}"> <a href="{% url "home" %}">{% trans "Home" %}</a> </li> {% endif %}
has_home – a boolean for whether a page object exists for the homepage, which is used to check whether a hard-coded link to the homepage should be used in the page menu
Makes sense, right?
{% if page.in_menu %} <li id="primary-menu-{{ page.html_id }}" class="{% if page.is_current_or_ascendant %}active{% endif %}{% if forloop.last %} last{% endif %}"> <a href="{{ page.get_absolute_url }}" {% if page.has_children_in_menu %} class='dropdown-button' data-activates='testmenu{{ page.html_id}}' {% endif %} >{{ page.title }}</a> {% if page.has_children_in_menu %} <div id='testmenu{{page.html_id}}' class='dropdown-content'> {% page_menu page %} </div> {% endif %} </li> {% endif %}
Now, it’s time to print out, the top level menu items. If the page is supposed to be in the menu, go ahead and print them out.
At this point, there’s a bit of deviation from how bootstrap works out its menus. In MaterializeCSS, I simply use Dropdowns for the menu. In fact, that’s the menu system. Works pretty nice to my liking. Less drama, and quickest to setup out of the box.
Therefore, the approach is this: If the top menu has_children_in_menu…
<a href="{{ page.get_absolute_url }}" {% if page.has_children_in_menu %} class='dropdown-button' data-activates='testmenu{{ page.html_id}}' {% endif %} >{{ page.title }}</a>
… then it should call the ‘dropdown-content’ classed element below, which holds the next level page branch items.
{% if page.has_children_in_menu %} <div id='testmenu{{page.html_id}}' class='dropdown-content'> {% page_menu page %} </div> {% endif %}
NOTE: All the immediate above two snippets need to be WITHIN the <li> … </li>.
The somehow magical behavior of the {% page_menu page %} is as follows. Lemme bring back the quote above:
“The
page_menu
template tag is responsible for rendering a single branch of the page tree at a time, and accepts two optional arguments. The arguments are the name of a menu template to use for a single branch within the page tree, and the parent menu item for the branch that will be rendered.”
Because of that, whenever you do:
{% page_menu page %}
You’re implicitly telling the {% page_menu %} to do this:
- Take the template_name of the context in which you were called, in this case, the ‘pages/menus/primary.html’
- Then run through it to render the page items.
But there’s a catch: BECAUSE we’re on branch 1 at this moment, when the {% page_menu %} renders through the ‘pages/menus/primary.html’, it jumps to the next block of code, after the {% else %}
At this moment, it’ll be rendering on the branch 1, thus ignoring the first part.
<ul class="right hide-on-med-and-down"> {% for page in page_branch %} {% if not has_home and page.is_primary and forloop.first %} <li id="primary-menu-home" class="first{% if on_home %} active{% endif %}"> <a href="{% url "home" %}">{% trans "Home" %}</a> </li> {% endif %} {% if page.in_menu %} <li id="primary-menu-{{ page.html_id }}" class="{% if page.is_current_or_ascendant %}active{% endif %}{% if forloop.last %} last{% endif %}"> <a href="{{ page.get_absolute_url }}">{{ page.title }}</a> </li> {% endif %} {% endfor %} <li class="divider-vertical"></li> </ul>
What you need to take note of is that I’m no longer using the {% page_menu %} tag in the {% else %} block. I think I had issues with maximum recursive imports with that. There was the merry-go-round kinda import, Thus, I simply access the attributes on the Page model in context, with the {{ page.get_absolute_url }} and the {{ page.title }}.
I possibly could use another {% if %} condition to check if I’m on branch 1 or whatnot, but with just only one level needed in my menu structure, I find this cleaner and shorter.
Done!
I put everything on Gist on Github, should you want to suggest improvements, or you can leave comments below