Wednesday, January 11, 2012

Django Navigation States

I've tried several approaches to implementing Django navigation correctly.  Each approach presents it's own unique challenges and limitations.  The toughest thing to get right is the state.  By state, I mean the menu item that's currently active. Active navigational items need to be conveyed visually - this is easy enough from Django's perspective because it often means inserting the correct CSS class in the template.

Furthering the complexity to designing solid navigation in Django user interfaces is the fact that we can have dynamic variables in the URL path.  This makes a simple equality test in determining the state arduous.

So with this in mind, let me demonstrate the best Django-friendly approach to implementing state management with navigation.  I say Django-friendly because my approach attempts to be of use for any Django application.

Overview
The approach I'm using is similar to this one.  I'm creating a template filter that's applied to the HTTP request object inside a given template.  The filter takes a parameter - an identifier for the item we want state applied to.  So long as the identifier is valid, we can use this filter to emit the appropriate CSS class.

The Template
Here is the navigation template.  Intentionally simple, it shows how the link_state filter is applied.

<ul>
    <li><a class="{{ request|link_state:'home' }}" >Home</a></li>
    <li><a class="{{ request|link_state:'products' }}" >Products</a></li>
    <li><a class="{{ request|link_state:'services' }}" >Services</a></li>
</ul>

The Filter
Here is the link_state template filter used in the template to apply navigation item state.

from django import template
from django.core.urlresolvers import reverse, resolve, NoReverseMatch

register = template.Library()

@register.filter
def link_state(request, key):
    
    keys = dict(
        home = (
            'home',
            'promotion_details',
        ),
        products = (
            'product_list',
            'product_details',
        ),
        services = (
            'service_list',
            'service_details',
        )
    )
    
    url = resolve(request.path)
    
    for candidate in keys.get(key, []):
        try:
            candidate = reverse(
                candidate,
                args=url.args,
                kwargs=url.kwargs
            )
        except NoReverseMatch:
            continue
        if candidate == request.path:
            return 'active'
    return 'default'

Explanation
The link_state filter works by taking the HTTP request path, the navigational item key, and comparing URLs.  If a URL in the set associated with the key matches the request path, the state is active.  Otherwise, the state is default.

The keys dictionary maps navigational item keys to their active state URLs.  Here, there are three keys - home, products, and services.  Notice the relation to these names and arguments passed to link_state in the template.  Each key stores a tuple of URL names - when you define a URL in Django, you can give it a name.

Next, we resolve the request path.  The reason for doing so is that we need any dynamic path values so when we do comparisons, we can match these URLs too. Now we can iterate through each URL in the specified key, checking for a match.

This approach is flexible because it allows for navigational state management even with dynamic path values, without the need for any specialized code.  For example, the product_details URL might look like /products/845/.  This URL would set the products link as active.