Django TemplateYaks

Yak shaving around Django Template Tags

Lately I've been writing more Django template tags than usual and last Thursday morning, I spotted a Mastodon Thread 🧵 about Django templates and composability.

As part of this thread, I mentioned the fact that "complex" (or advanced) templatetags were a pain to write and that one thing that was missing for Django templates to be composable was a version of {% include %} that supports "blocks" (ie: something like {% includeblock %}...{% endincludeblock %}).

Post by @EmmaDelescolle@mastodon.social
View on Mastodon

I stand by what I said. This is nothing new! The documentation on the subject is lengthy but sparse. I would say people have found writing templatetags painful for a long time now (see: django-templatetag-sugar). Of course there are Simple tags and Inclusion tags that will probably fit over 75% of use-cases. But once you need to write a block tag , it's time to start scratching your head!

What is not very obvious from the docs though is that those 2, among other things, make use of undocumented helper functions and Nodes that could be of use by anyone writing templatetags.

Writing a complex tag involves 2 main steps:

  • parsing (the compile function) which recieves the raw token (ie: the string between {% and %} in your template). The documentation tells you about token.split_contents() that can split that string into meaningfull bits but, that's about it... Have you ever heard of the parse_bits function? Or the internal helper function that calls it
  • rendering (using a Node). Several Node types exist in Django's source code but the documentation only mentions the template.Node. I would argue that over half of the people writing advanced tags could make use of SimpleNode or TemplateNode.

So, the documentation could be better but, could there be a way to make the whole process easier too, especially for block-type tags?

Class-based Template Tags?

In order to make the most of re-usable code, I decided to try an object-oriented approach to writing template tags. Something that would be extensible and re-usable all the while being more customizable without being more complicated than simple_tag and inclusion_tag.

Basically, the same idea that was behind Class-Based Views. (And, yes, this comes with its own CCBV-style documentation).

So I wrote a proof of concept... It provides developpers with all the tools used by simple_tag and inclusion_tag so one doesn't have to rewrite things like token parsing every time they need to write an advanced template tag, and more.

If you want to look at the internals, you'll notice there is very little logic in there that was not borrowed from Django's own code! It's mostly just organized in a more re-usable way.

This is how it works for a few of the examples provided by Django's documentation.

Simple Tag

from django import template
from yak.templatetag import TemplateTag


register = template.Library()


class MyTag(TemplateTag):
    def render(self):
        return 'My tag'


register.tag(MyTag.as_tag())

Inclusion Tag

class JumpLink(TemplateTag):
    template_name = 'link.html'

    def render(self, context):
        return {
            'link': context['home_link'],
            'title': context['title'],
        }

Advanced example

class Upper(TemplateTag):
    is_block_node = True

    def render(self, context):
        inner_text = self.nodelist.render(context)
        return inner_text.upper()

Real life example

To test the concept of class-based templatetags I wrote a small Komponent library (YAK in this article stands for Yet Another Komponent).

Here is a simpler version of the code (support for named slots was removed in this example).

Have a look at the code and then ask yourself how easy or hard would it have been to write something similar following the instructions in Django's documentation.

from .tags import TemplateTag, InclusionNode


class Komponent(TemplateTag):
    is_block_node = True
    node_class = InclusionNode
    accepts_with_context = True

    def clean_bits(self, bits):
        '''
        Stores the `FilterExpression` representing the
        template name during parsing.
        This expression will have be evaluated by the `Node`
        at render time.
        '''

        super().clean_bits(bits)
        self.template_name = \
            self.parser.compile_filter(bits.pop(0))

    def render(self, context):
        '''
        Stores the rendered contents of the internal node list
        in the context as `yield` alongside with any
        evaluted `with_context`.

        A convenience `has_block` variable is also set to
        represent whether or not there is something in `yield`.
        '''

        slot_content = self.nodelist.render(context)

        return {
            # makes a copy of `context` so there is no risk
            # of it getting modified
            **context.flatten(),
            # `get_with_context` evaluates variables assigned
            # using the "with" keyword
            **self.get_with_context(context),
            'yield': slot_content,
            'has_block': len(slot_content.strip()) > 0,
        }

source code with comments and named slots support

Note
This library is about class-based templatetags, The included comnponent system is more of a sample implementation and is quite barebone (even with named slots). If you're looking for something more complete you might want to take a look at django-bird, django-components or slippers.

Also checkout dj-angles as a complement to any component library.

Feedback on this would be greatly appreciated, even more so from people who have written their own Django component library.

You might also like

Recently Andy Miller - AKA nanorepublica posted in various media about missing Django batteries, including but not limited to this forum thread and articles series.

He's not the only one, I have seen similar posts from several other people in my circles.

Here I'll try to explain my take on the original forum question

Recently Andy Miller - AKA nanorepublica posted in various media about missing Django batteries, including but not limited to this forum thread and articles series.

He's not the only one, I have seen similar posts from several other people in my circles.

Here I'll try to explain my take on the original forum question

Recently Andy Miller - AKA nanorepublica posted in various media about missing Django batteries, including but not limited to this forum thread and articles series.

He's not the only one, I have seen similar posts from several other people in my circles.

Here I'll try to explain my take on the original forum question

Comments

(via Mastodon or BlueSky )
Loading...
Comment via Mastodon Comment via BlueSky