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 %}).
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 abouttoken.split_contents()that can split that string into meaningfull bits but, that's about it... Have you ever heard of theparse_bitsfunction? 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 thetemplate.Node. I would argue that over half of the people writing advanced tags could make use ofSimpleNodeorTemplateNode.
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.
Comments
(via Mastodon or BlueSky )