Template Usage#

Guide to using django-flex-menus template tags.

Template Tags#

Load the template tag library:

{% load flex_menu %}

render_menu#

Render a complete menu by name or instance. Media (CSS/JS) is automatically included.

{% render_menu 'menu_name' renderer='bootstrap5' %}

If you are providing a menu as a context variable to your template, you can pass it directly:

{% render_menu menu_object renderer='bootstrap5' %}

Disable media inclusion:

Sometimes you may want to include CSS/JS manually or only once per page. Disable automatic media inclusion with include_media=False:

{% render_menu 'menu_name' renderer='bootstrap5' include_media=False %}

Extra kwargs:

You can pass any number of keyword arguments to render_menu. These arguments are used in two ways:

{% render_menu 'menu_name' renderer='bootstrap5' pk=object.pk project=project user=owner %}

Kwargs are handled in the following ways:

  1. Check functions - All kwargs are passed unaltered to check functions to enable context-aware visibility decisions.

  2. URL functions - All kwargs are passed unaltered to url functions to enable context-aware url resolution.

  3. View resolution - Only kwargs specific to the specified view_name are passed to Django’s reverse() function for URL resolution.

See Visibility Checks and Dynamic URL Resolution for details.

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.

Multiple Menus#

{# Top navigation #}
<nav class="navbar">
    {% render_menu 'top_nav' renderer='navbar' %}
</nav>

{# Sidebar navigation #}
<aside class="sidebar">
    {% render_menu 'sidebar_nav' renderer='sidebar' %}
</aside>

{# Footer links #}
<footer>
    {% render_menu 'footer_nav' renderer='simple' %}
</footer>

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.

Context-Aware Menus#

Pass context variables to control visibility based on database objects:

{# In a project detail view #}
<div class="project-header">
    <h1>{{ project.name }}</h1>
    {% render_menu 'project_actions' renderer='bootstrap5' project=project %}
</div>
# Menu definition with context-aware checks
from flex_menu import Menu, MenuItem

def can_edit_project(request, project=None, **kwargs):
    if not project or not request.user.is_authenticated:
        return False
    return project.owner == request.user

Menu(
    name="project_actions",
    children=[
        MenuItem(name="view", view_name="project_detail"),
        MenuItem(name="edit", view_name="project_edit", check=can_edit_project),
        MenuItem(name="delete", view_name="project_delete", check=can_edit_project),
    ],
)

See Context-Aware Checks for more examples.

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:

  1. Django-flex-menus inspects each URL pattern to determine required parameters

  2. Only matching parameters from kwargs are passed to reverse()

  3. All other parameters are still available to check functions

  4. 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_detail and user_edit and user_delete get pk=user.pk for URL resolution

  • user_list gets no parameters (doesn’t need any)

  • All check functions receive all kwargs: pk, user_obj, and current_user

  • The delete check can use both user_obj and current_user for 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>

Processed Menu Properties#

After process_menu, items have these properties:

Navigation:

  • visible_children - List of visible child items

  • parent - Parent MenuItem

  • depth - Depth in tree (0, 1, 2, …)

State:

  • visible - Whether item passed visibility checks

  • selected - Whether item matches selection path

  • url - Resolved URL (or None)

Type checks:

  • has_url - Has a resolvable URL

  • has_children - Has child items

  • is_leaf - No children

  • is_parent - Has children

  • is_clickable - Has resolvable URL

Data:

  • name - Unique identifier

  • extra_context - Dict of custom data

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#

  1. Load once - Load {% load flex_menu %} at the top of your template

  2. Specify renderers - Always include renderer names in render_menu tags

  3. Named renderers - Configure renderers in settings, reference by name

  4. Process when needed - Use process_menu only for custom rendering logic