Custom Renderers#
Build custom renderers to control menu HTML output.
Renderer Basics#
Renderers transform processed menu items into HTML. They define:
Templates - Which template to use for each item
Context - What data to pass to templates
Media - CSS/JS files to include
Creating a Renderer#
Extend BaseRenderer:
from flex_menu.renderers import BaseRenderer
class MyRenderer(BaseRenderer):
"""Custom renderer for my app."""
templates = {
0: {"default": "menus/container.html"},
1: {
"parent": "menus/dropdown.html",
"leaf": "menus/link.html",
},
"default": {
"parent": "menus/nested_dropdown.html",
"leaf": "menus/nested_link.html",
},
}
Template structure:
Keys are depth levels (0, 1, 2, …) or
"default"Values are dicts with
"parent","leaf", or"default"keys"parent"= items with children"leaf"= items without children"default"= fallback for both types
Depth-based Templates#
Different templates for different nesting levels:
templates = {
# Container (top-level menu)
0: {"default": "menus/nav.html"},
# First level items
1: {
"parent": "menus/dropdown.html", # Has children
"leaf": "menus/nav_link.html", # No children
},
# Second level items
2: {
"parent": "menus/submenu.html",
"leaf": "menus/submenu_link.html",
},
# Deeper levels (fallback)
"default": {
"parent": "menus/deep_parent.html",
"leaf": "menus/deep_link.html",
},
}
Adding Media#
Include CSS and JavaScript:
class MyRenderer(BaseRenderer):
templates = {
# ... templates ...
}
class Media:
css = {
'all': ('menus/styles.css',)
}
js = ('menus/scripts.js',)
Custom Context Data#
Override get_context_data() to add custom template variables:
class MyRenderer(BaseRenderer):
templates = {
# ... templates ...
}
def get_context_data(self, item, **kwargs):
"""Add custom context for templates."""
context = super().get_context_data(item, **kwargs)
# Add custom data
context['custom_class'] = self.get_item_class(item)
context['icon'] = item.extra_context.get('icon')
return context
def get_item_class(self, item):
"""Generate CSS class for item."""
classes = ['menu-item']
if item.selected:
classes.append('active')
if item.has_children:
classes.append('has-dropdown')
return ' '.join(classes)
Handling Special Cases#
Override get_template() for special item types:
class MyRenderer(BaseRenderer):
templates = {
# ... templates ...
}
def get_template(self, item):
"""Select template based on item properties."""
# Check for divider
if item.extra_context.get('divider'):
return "menus/divider.html"
# Check for special items
if item.extra_context.get('heading'):
return "menus/heading.html"
# Default behavior
return super().get_template(item)
Complete Example#
Tailwind CSS renderer:
from flex_menu.renderers import BaseRenderer
class TailwindRenderer(BaseRenderer):
"""Tailwind CSS menu renderer."""
templates = {
0: {"default": "menus/tailwind/container.html"},
1: {
"parent": "menus/tailwind/dropdown.html",
"leaf": "menus/tailwind/nav_link.html",
},
"default": {
"parent": "menus/tailwind/nested_dropdown.html",
"leaf": "menus/tailwind/nested_link.html",
},
}
class Media:
css = {
'all': ('https://cdn.tailwindcss.com',)
}
def get_context_data(self, item, **kwargs):
"""Add Tailwind-specific context."""
context = super().get_context_data(item, **kwargs)
# Add CSS classes
context['item_classes'] = self.get_item_classes(item)
context['link_classes'] = self.get_link_classes(item)
return context
def get_item_classes(self, item):
"""Generate Tailwind classes for list item."""
classes = ['relative']
if item.has_children:
classes.append('group')
return ' '.join(classes)
def get_link_classes(self, item):
"""Generate Tailwind classes for link."""
classes = [
'block', 'px-4', 'py-2',
'text-gray-700', 'hover:bg-gray-100',
]
if item.selected:
classes.extend(['bg-blue-50', 'text-blue-600'])
return ' '.join(classes)
Template Examples#
Container template (menus/tailwind/container.html):
<nav class="bg-white shadow">
<ul class="flex space-x-4">
{% for child in item.visible_children %}
{% render_item child renderer=renderer %}
{% endfor %}
</ul>
</nav>
Link template (menus/tailwind/nav_link.html):
<li class="{{ item_classes }}">
<a href="{{ item.url }}" class="{{ link_classes }}">
{{ item.name }}
</a>
</li>
Dropdown template (menus/tailwind/dropdown.html):
<li class="{{ item_classes }}">
<button class="{{ link_classes }} flex items-center">
{{ item.name }}
<svg class="w-4 h-4 ml-1"><!-- dropdown icon --></svg>
</button>
<ul class="absolute hidden group-hover:block bg-white shadow-lg">
{% for child in item.visible_children %}
{% render_item child renderer=renderer %}
{% endfor %}
</ul>
</li>
Registering Your Renderer#
Add to settings.py:
FLEX_MENUS = {
"renderers": {
"tailwind": "myapp.renderers.TailwindRenderer",
"bootstrap5": "myproject.renderers.Bootstrap5NavbarRenderer",
},
}
Use in templates:
{% render_menu 'main_nav' renderer='tailwind' %}
{% render_menu 'main_nav' renderer='tailwind' %}
Error Handling#
Raise errors for unsupported depths:
class SimpleRenderer(BaseRenderer):
"""Renderer that only supports 2 levels."""
templates = {
0: {"default": "menus/container.html"},
1: {"parent": "menus/dropdown.html", "leaf": "menus/link.html"},
}
def get_template(self, item):
"""Only allow up to depth 1."""
if item.depth > 1:
raise ValueError(
f"SimpleRenderer only supports up to 2 nesting levels. "
f"Item '{item.name}' is at depth {item.depth}."
)
return super().get_template(item)
Advanced: Renderer Instance#
Access the renderer instance in templates via {{ renderer }}:
class MyRenderer(BaseRenderer):
templates = {
# ... templates ...
}
def get_dropdown_id(self, item):
"""Generate unique ID for dropdown."""
return f"dropdown-{item.name}"
def get_context_data(self, item, **kwargs):
context = super().get_context_data(item, **kwargs)
context['renderer'] = self # Make renderer available
return context
In template:
<li>
<button data-target="#{{ renderer.get_dropdown_id(item) }}">
{{ item.name }}
</button>
<ul id="{{ renderer.get_dropdown_id(item) }}">
<!-- children -->
</ul>
</li>
Best Practices#
Inherit from BaseRenderer - Don’t start from scratch
Use depth-based templates - Define clear structure for each level
Provide Media - Include necessary CSS/JS
Override minimally - Only override what you need to customize
Document depth limits - Make clear how many levels are supported
Test thoroughly - Test with different menu structures
Use extra_context - Support custom item attributes
Keep templates simple - Move logic to renderer methods