Jinja2 Custom Template Tags

Posted on Tue 23 March 2021 in posts

Building custom template tags in Jinja2 is not an easy task. That used to be the case, which you will see below.

tl;dr: Give jinja2-simple-tags a try.

Why would you create a custom template tag?

From the docs:

By writing extensions you can add custom tags to Jinja. This is a non-trivial task and usually not needed as the default tags and expressions cover all common use cases.

Flask-Meld uses custom template tags to extend the Jinja parser to look for meld tags. A meld tag will look like {% meld 'search' %} where search is the name of a component. When Jinja encounters a meld tag, it generates the HTML from the component template and inserts the HTML into the template.

Why is creating a custom template tag in Jinja2 so hard?

To parse custom tags, you need to extend the Jinja template parser to look for your tags. Once the parser locates your tag, you need to instruct the parser to update the compiler's Abstract Syntax Tree.

The original Flask-Meld tag was created by simply looking at other examples.

Here's the first iteration of the MeldTag extension:

class MeldTag(Extension):
    """
    Create a {% meld %} tag.
    Used as {% meld 'component_name' %}
    """
    tags = {"meld"}

    def parse(self, parser):
        lineno = parser.stream.expect("name:meld").lineno
        component = parser.parse_expression()
        call = self.call_method("_render", [component], lineno=lineno)
        return nodes.Output([nodes.MarkSafe(call)]).set_lineno(lineno)

    def _render(self, component):
        mn = MeldNode(component)
        return mn.render()

When the pain starts to hit..

Soon it became clear that allowing arguments in a template tag would be a valuable feature for Flask-Meld. That tag might look like this: {% meld 'deploy_site', site_id=site.id %} and will render the deploy_site component and pass along the site id as site_id.

I took to the internet to find an example and came up surprisingly empty-handed. Then I realized Jinja has an assortment of template tags that are built-in. Maybe I can find an example in their codebase? The Jinja repo has an example of an extension that uses a custom tag with an example: {% trans count=something() %}. It's a little more than I was looking for, but nothing we can't make a quick modification to and get our implementation working, right?

If you're interested, you can take a look at the 113 line parse function for the custom tag.

Remember earlier when I mentioned the Abstract Syntax Tree?

The AST (Abstract Syntax Tree) is used to represent a template after parsing. It's build of nodes that the compiler then converts into executable Python code objects. Extensions that provide custom statements can return nodes to execute custom Python code.

Building a compiler might be fun the first time, but I'm not here for that right now. I don't want to learn how Jinja parsed and compiled templates or uses an Abstract Syntax Tree to represent a template. Flask-Meld just needs a custom tag that accepts arguments...

Creating a custom template tag that accepts arguments

What does it take to create a simple tag, that looks like this {% meld 'deploy_site', site_id=site.id %} and parses kwargs?

Given no other options at the time, I set out against my will to start extending the current meld tag implementation, but didn't quite get it working. It looked like this:

def parse(self, parser):
    args = None
    kwargs = None
    while parser.stream.current.type != lexer.TOKEN_BLOCK_END:
        token = parser.stream.current
        if token.test('name:meld'):
            parser.stream.expect(lexer.TOKEN_NAME)
            component_name = parser.stream.expect(lexer.TOKEN_STRING)
            lineno = component_name.lineno
        elif kwargs is not None:
            kwargs.append(self.parse_expression(parser))
        elif parser.stream.look().type == lexer.TOKEN_ASSIGN:
            kwargs = {}

    call = self.call_method("_render", args=[component_name, args, kwargs], lineno=lineno)
    return nodes.Output([nodes.MarkSafe(call)]).set_lineno(lineno)

Jinja2 Simple Tags

After some additional searching, I went to PyPi and looked for simple custom tags. A library with zero stars and was updated days ago that claims to do what I need? Thank you, thank you, thank you! The jinja2-simple-tags allowed me to create the custom tag that I wanted, and it was so easy! Using the code above as a reference, let's see what the tag looks like using jinja2-simple-tags

class MeldTag(StandaloneTag):
    tags = {"meld"}

    def render(self, component_name, **kwargs):
        mn = MeldNode(component_name)
        return mn.render(**kwargs)

Seriously, a few lines of code, no parsing function, no Abstract SyntaxTree.

Big shout out to the creator of jinja2-simple-tags. Whether you're building a Flask extension or need some extra functionality inside your jinja templates, you will find the jinja2-simple-tags library a big help.