Template Usage#
Guide to using django-flex-menus template tags.
Common Patterns#
Base Template#
{% load flex_menu %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
<header>
{% render_menu 'main_nav' renderer='navbar' %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% render_menu 'footer_nav' renderer='simple' %}
</footer>
</body>
</html>
Note
Media (CSS/JS) is automatically included before each menu.
Active Item Highlighting#
Active items are automatically determined. An item configured with view_name is matched against the request’s resolved view name, so it stays active for query strings, trailing-slash variants, other objects served by the same view, and translated URLs. An item configured with a literal url or a callable has no resolved view to compare against, so it falls back to comparing its URL to request.path, ignoring any query string, fragment and trailing slash. When an item matches, its selected property is set to True.
Renderers typically add CSS classes like active or selected to items where item.selected is True.
Dynamic URL Resolution#
Context parameters are also used for URL resolution, allowing menu items to adapt URLs based on the current view context.
Automatic Parameter Filtering for view_name
When using view_name, django-flex-menus automatically filters kwargs to only include parameters that the URL pattern actually requires. This means you can pass extra context without worrying about breaking URL resolution:
{# Pass many kwargs - only 'pk' will be used for URL resolution #}
{% render_menu 'post_actions' renderer='bootstrap5' pk=post.pk project=project user=user %}
# URL patterns
urlpatterns = [
path('posts/<int:pk>/edit/', views.edit_post, name='post_edit'),
path('posts/<int:pk>/delete/', views.delete_post, name='post_delete'),
]
# Menu definition - URLs are resolved using only the pk parameter
Menu(
name="post_actions",
children=[
MenuItem(name="edit", view_name="post_edit"), # Becomes /posts/123/edit/
MenuItem(name="delete", view_name="post_delete"), # Becomes /posts/123/delete/
],
)
How Parameter Filtering Works:
Django-flex-menus inspects each URL pattern to determine required parameters
Only matching parameters from kwargs are passed to
reverse()All other parameters are still available to check functions
If a required parameter is missing, the URL resolution fails gracefully
With callable URLs:
Callable URLs receive all kwargs, allowing full flexibility:
def comment_url(request, comment_id=None, **kwargs):
"""Generate URL for a specific comment."""
# All kwargs are available here
post_pk = kwargs.get('pk', request.resolver_match.kwargs.get('pk'))
return f"/posts/{post_pk}/comments/{comment_id}/"
Menu(
name="comment_actions",
children=[
MenuItem(name="edit_comment", url=comment_url),
],
)
Practical Examples:
# Complex menu with different URL parameter requirements
Menu(
name="admin_actions",
children=[
# Needs 'pk' parameter
MenuItem(name="view_user", view_name="user_detail", check=user_is_staff),
# Needs 'pk' parameter
MenuItem(name="edit_user", view_name="user_edit", check=user_is_superuser),
# No parameters needed
MenuItem(name="user_list", view_name="user_list", check=user_is_staff),
# Custom check using multiple context vars
MenuItem(
name="delete_user",
view_name="user_delete",
check=lambda request, user_obj=None, **kwargs: (
request.user.is_superuser and
user_obj and
user_obj != request.user
)
),
],
)
{# Template: Pass all context - each URL gets only what it needs #}
{% render_menu 'admin_actions' renderer='bootstrap5' pk=user.pk user_obj=user current_user=request.user %}
In this example:
user_detailanduser_editanduser_deletegetpk=user.pkfor URL resolutionuser_listgets no parameters (doesn’t need any)All check functions receive all kwargs:
pk,user_obj, andcurrent_userThe delete check can use both
user_objandcurrent_userfor complex logic
{% render_menu 'comment_actions' renderer='simple' comment_id=comment.id %}
Combined visibility and URL resolution:
def can_edit_project(request, project=None, **kwargs):
if not project:
return False
return project.owner == request.user
Menu(
name="project_menu",
children=[
# Same 'project' param used for both check and URL resolution
MenuItem(
name="edit",
view_name="project_edit", # Resolves to /projects/{project.pk}/edit/
check=can_edit_project, # Receives project for visibility check
),
],
)
{% render_menu 'project_menu' renderer='bootstrap5' project=project pk=project.pk %}
Important
If you pass kwargs that aren’t needed by a URL pattern, Django’s reverse() will fail and that menu item will become invisible (not throw an error). This allows you to pass multiple context variables where different items use different subsets:
Menu(
name="actions",
children=[
MenuItem(name="list", view_name="posts"), # No kwargs needed
MenuItem(name="detail", view_name="post_detail"), # Uses 'pk'
MenuItem(name="edit", view_name="post_edit"), # Uses 'pk'
],
)
{# Pass pk - 'list' item stays visible (no view_name kwargs),
'detail' and 'edit' get pk for URL resolution #}
{% render_menu 'actions' renderer='simple' pk=post.pk %}
Custom Rendering#
For complete control, process the menu and render manually:
{% process_menu 'main_nav' as menu %}
<ul class="custom-menu">
{% for item in menu.visible_children %}
<li class="{% if item.selected %}active{% endif %}">
{% if item.has_url %}
<a href="{{ item.url }}">{{ item.name }}</a>
{% else %}
<span>{{ item.name }}</span>
{% endif %}
{% if item.visible_children %}
<ul class="submenu">
{% for child in item.visible_children %}
<li><a href="{{ child.url }}">{{ child.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
Caching#
Processed menus are cached per request to avoid redundant processing:
{# First call: processes menu #}
{% render_menu 'main_nav' %}
{# Second call: uses cached result #}
{% render_menu 'main_nav' %}
Cache is request-scoped and thread-safe.
Renderer Selection#
Renderers must be specified in the template tag:
{% render_menu 'nav' renderer='navbar' %}
If no renderer is specified, the tag will raise an error.
Configure available renderers in settings.py:
FLEX_MENUS = {
"renderers": {
"bootstrap5": "myproject.renderers.Bootstrap5NavbarRenderer",
"sidebar": "myproject.renderers.Bootstrap5SidebarRenderer",
"simple": "myproject.renderers.SimpleHTMLRenderer",
},
}
Error Handling#
If a menu doesn’t exist, render_menu raises TemplateSyntaxError:
{% render_menu 'nonexistent' %}
{# Raises: Menu 'nonexistent' does not exist #}
To handle gracefully:
{% process_menu 'optional_menu' as menu %}
{% if menu %}
{% render_item menu %}
{% endif %}
Templates rendered without a request#
Both tags draw nothing when the template context carries no request. Neither raises.
This matters most for error pages. Django renders 500.html through
django.views.defaults.server_error, which calls template.render() with no context and
no request at all. A base template that draws a menu is therefore rendered without one
every time the site returns a 500, and a tag that raised there would replace the error you
need to read with its own.
The same applies to render_to_string() called without a request, to a management command
that renders a template, and to any other request-free render. Where a menu does matter on
such a page, pass the request in yourself:
render_to_string("report.html", {"request": request})
A template naming a menu that does not exist still raises, with or without a request. Visibility checks are never called without one, so a check function can keep assuming it has a request.
Best Practices#
Load once - Load
{% load flex_menu %}at the top of your templateSpecify renderers - Always include renderer names in
render_menutagsNamed renderers - Configure renderers in settings, reference by name
Process when needed - Use
process_menuonly for custom rendering logic