March 20, 2025

Django Template components are slowly coming

The problem before me last week was to build out the navigation for a section of the new startup application. I was about to install a template component library (django-bird was the choice...and I still might!), when I noticed in an issue on django-bird that coming in Django 5.2 is the simple_block_tag template tag. After reviewing the example in the docs I thought it would be a perfect fit for my use case. The result was 3 template tags that allow me to build out a navigation that has toggles with sub items and tracking the currently active page.

First here is the final template in which they are used:

{% navitem url_name='thing-dashboard' %}Dashboard{% endnavitem %}
{% togglenavitem toggle_name="THINGS" %}
  {% subnavitem url_name='thing-list' %}Overview{% endsubnavitem %}
  {% for thin in things %}
    {% subnavitem url_name=account.get_absolute_url %}Thing {{ thin.pk }}{% endsubnavitem %}
  {% endfor %}
  {% subnavitem url_name='thing-create' %}
  New thing
  {% endsubnavitem %}
{% endtogglenavitem %}

This has a top-level navigation item and a toggle which lists a list of "things" with option of creating a new thing at the end. Let's start simple with the main nav item. The template tag is as follows:

@register.simple_block_tag(takes_context=True)
def navitem(context, content, url_name):
    url = resolve_url(url_name)
    is_active = context['request'].path == url
    format_kwargs = {
        "content": content,
        "url": url,
        "is_active_classes": "bg-blue-50" if is_active else "bg-gray-50 hover:bg-blue-50"
    }
    return get_template("nav/item.html").render(format_kwargs)

and the referenced template being:

  <li>
    <a href="{{url}}"
       class="block rounded-md py-2 pl-10 pr-2 text-sm/6 font-semibold text-gray-700 {{is_active_classes}}">{{content}}</a>
  </li>

It's really quite simple, almost like a sub-view? We simple construct the necessary variables for the template and then render it! The other two template tags are very similar as shown below for completeness.

@register.simple_block_tag(takes_context=True)
def subnavitem(context, content, url_name):
    url = resolve_url(url_name)
    is_active = context['request'].path == url
    format_kwargs = {
        "content": content,
        "url": url,
        "is_active_classes": "bg-blue-50 is_active" if is_active else "bg-gray-50 hover:bg-blue-50"
    }
    return get_template("nav/subitem.html").render(format_kwargs)


@register.simple_block_tag(takes_context=True)
def togglenavitem(context, content, toggle_name):
    format_kwargs = {
        "content": content,
        "toggle_name": toggle_name,
        "toggle_checked": "checked" if 'is_active' in content else ""
    }
    return get_template("nav/togglenavitem.html").render(format_kwargs)

The subnavitem template:

<li>
  <a href="{{url}}"
      class="block rounded-md py-2 pl-9 pr-2 text-sm/6 text-gray-700 {{is_active_classes}}">{{content}}</a>
</li>

The togglenavitem template:

    <li>
      <div>
        <input type="checkbox" id="toggle_{{toggle_name}}" class="peer hidden" {{toggle_checked}} />
        <label for="toggle_{{toggle_name}}"
                class="flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 peer-checked:[&>svg]:rotate-90 peer-checked:[&>svg]:text-gray-500"
                aria-controls="sub-menu-1"
                aria-expanded="false">
          <svg class="size-5 shrink-0 text-gray-400 transition-transform duration-200"
               viewBox="0 0 20 20"
               fill="currentColor"
               aria-hidden="true"
               data-slot="icon">
            <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
          </svg>
          {{toggle_name}}
        </label>
        <ul class="mt-1 px-2 overflow-hidden max-h-0 peer-checked:max-h-96 transition-all duration-200" id="sub-menu-1">
            {{content}}
        </ul>
      </div>
    </li>

We use a hidden checkbox to store the state if the toggle is open and the CSS class is_active to track if a subnavitem is the active page to keep the relevant toggle open.

Some final thoughts, first thanks to Jake Howard for proposing and implementing this change! Second he has proposed a similar tag for inclusion tags which would reduce the small amount of boilerplate in the template rendering. I do wonder however if it's possible to ditch writing any python templatetags at all? Could I specify the formatting in the template itself? That's probably where a third-party package comes in for now, which I will be happy to do, but until then I'm going to see how far I can go with the simple_block_tag!