Django example: creating a custom block template tag

Sometimes while you’re building a site using Django you may notice that you’re repeating the same chunk of HTML over and over again on different pages. Depending on the complexity of the HTML, that could create a maintenance nightmare. In this example I’m going to show how you can wrap up that HTML block and reuse it without repeating it using a custom block template tag.

The goal in this example is to build a sort of sub-template that can be reused on any template page. In some cases it may be sufficient to have a snippet that is added to another template using the {% include %} block, but since what you can pass into that block is so limited we’ll sometimes need to roll our own.

Toys lined up
Template tags can be reused in different contexts… much like these toys.
Image credit: Vanessa Bucceri via Unsplash.

Block tags differ from regular template tags because in order to invoke them you have to use at least 2 tags (a beginning and an end) and any code you put between them can be interpreted and altered by the tags. You’re probably familiar with block tags such as {% block %}{% endblock %}, {% for %}{% endfor %}, and {% if %}{% endif %}.

In this example we’ll create a template tag that will auto-generate all of the HTML necessary to display a Bootstrap modal using custom template block tags {% modal %} and {% endmodal %}.

The basic block tag

Let’s start by creating a basic template tag. This tag won’t do anything special with the contents passed in – it’s just going to display them exactly as-is.

from django import template
register = template.Library()

@register.tag
def modal(parser, token):
    nodelist = parser.parse(('endmodal',))
    parser.delete_first_token()
    return ModalNode(nodelist)

class ModalNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

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

Let’s break down what’s happening with this block. First the @register.tag decorator tells the Django template parser to watch out for a template tag that matches the name of the method, {% modal %} in this case. and execute this code when it sees it. Next nodelist = parser.parse(('endmodal',)) combined with parser.delete_first_token() tells the parser to get all the contents from {% modal %} to {% endmodal %} and delete them from the template node list. Then we’ll pass those contents along to the ModalNode class.

Note: if you can’t or don’t want to name your method the exact same name as your tag (for example, if there’s already another method with that name or the tag name is a Python reserved word) you can pass a parameter of the tag name to the decorator like @register.tag('modal'). Convention is then that your method name should start with ‘do’, like def do_modal().

Inside the ModalNode class we’re simply telling the render engine to display the nodes that were inside the modal block tags.

Now in the template we can have some code like this:

{% load name_of_tags_file %}

{% modal %}
    <h1&gt;Look ma! I'm in a modal!</h1&gt;
{% endmodal %}

Right now this isn’t going to do anything very exciting – it will just show the <h1> content as-is in the same place you have it in the template. So let’s get it inside that modal now.

Update the ModalNode class to render HTML that uses the Bootstrap modal pattern.

class ModalNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        output = \
            '< div class="modal fade" id="myModal" tabindex="-1" role="dialog" ' \
            '    aria-labelledby="myModalTitle" aria-hidden="true"&gt;' \
            '    < div class="modal-dialog modal-dialog-centered" role="document"&gt;' \
            '        < div class="modal-content"&gt;' \
            '            < div class="modal-header"&gt;' \
            '                < h5 class="modal-title" id="myModalTitle"&gt;Modal title</h5&gt;' \
            '                < button type="button" class="close" data-dismiss="modal" aria-label="Close"&gt;' \
            '                    < span aria-hidden="true"&gt;×</span&gt;' \
            '                </button&gt;' \
            '             </div&gt;' \
            '             < div class="modal-body"&gt;' \
            f'                {self.nodelist.render(context)}' \
            '            </div&gt;' \
            '        </div&gt;' \
            '    </div&gt;' \
            '</div&gt;'
        return output

Phew! And we now have a modal! Note that buried there in the modal-body block we have a formatted text block (don’t forget the f at the beginning of that line!) to insert the nodes insider your block tags.

Note: You might notice in the code block above that there are spaces in the HTML tags. If you copy this code please remove those spaces – they’re only there because WordPress chokes on that block without them. 😕

Now we have just a couple of problems with this new modal. The first problem is that we can only have one of these modals on a page since we’ve hard-coded the ID of the modal. The next problem is that the title of the modal is also hard-coded, so all modals will have the title “Modal title”.

Passing variables

Let’s fix these problems by passing some variables to our tags. We want to pass the ID and title from the template like this:

{% modal 'lookMaModal' 'Look ma!' %}
    <h1&gt;I'm in a modal!</h1&gt;
{% endmodal %}

In this example the first argument is the ID of the modal window and the second argument is the title. We’ll have to train the modal() method to accept those arguments and pass them along now.

@register.tag
def modal(parser, token):
    try:
        tag_name, modal_id, title = token.split_contents()
    except ValueError:
        raise TemplateSyntaxError("%r takes two arguments: the modal id and title" % token.contents.split()[0])

    nodelist = parser.parse(('endmodal',))
    parser.delete_first_token()
    return ModalNode(modal_id, title, nodelist)

Here token.split_contents() looks at everything passed along in the {% modal %} tag and splits it into a list where modal, the tag name, is always the first item. If we don’t get 2 arguments in the list in addition to the tag name we throw a TemplateSynatxError. If you prefer, you could set some defaults instead.

We’ll need to update the ModalNode class to accept these new parameters and use them.

class ModalNode(template.Node):
    def __init__(self, modal_id, title, nodelist):
        self.modal_id = modal_id
        self.title = title
        self.nodelist = nodelist

    def render(self, context):
        modal_id = self.modal_id.resolve(context) if self.modal_id else 'modal'
        title = self.title.resolve(context) if self.title else None
        output = \
            f'< div class="modal fade" id="{self.modal_id}" role="dialog" aria-labelledby="{self.modal_id}Title" aria-hidden="true">' \
            '    < div class="modal-dialog modal-dialog-centered" role="document">' \
            '        < div class="modal-content">' \
            '            < div class="modal-header">' \
            f'                < h5 class="modal-title" id="{self.modal_id}Title">Modal title</h5>' \
            '                < button type="button" class="close" data-dismiss="modal" aria-label="Close">' \
            '                    < span aria-hidden="true">×</span>' \
            '                </button>' \
            '             < /div>' \
            '             < div class="modal-body">' \
            f'                {self.nodelist.render(context)}' \
            '            </div>' \
            '        </div>' \
            '    </div>' \
            '</div>'
        return output

Great! With that we now have a modal with a dynamic ID and title. Now we can update our template code to have a button that targets the modal using the passed-in ID.

Note that in the first couple of lines in the render() method we’re resolving the passed-in parameters. For string values this will just remove the quotes around the string, but if you were to pass a variable along from your template it will resolve the variable instead of just displaying the name of the variable.

Same note as above applies – if you’re going to copy this code please remember to remove the spaces in the tags! Thanks!

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#lookMaModal"&gt;
  Look ma!
</button&gt;

{% modal "lookMaModal" "Look ma!" %}
    <h1&gt;I'm in a modal!</h1&gt;
{% endmodal %}
Modal pop-up generated by our code

You can include any valid Django template code inside your block tag; for example, if you wanted to have a modal that included a form to add a note to a page your template code would look like this:

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#noteModal"&gt;
  + Add a note
</button&gt;

{% modal "formModal" "Enter your note" %}
    <form method="POST" action=""&gt;
        {% csrf_token %}
        {{ form }}
        <button class="btn btn-primary" type="submit"&gt;Save</button&gt;
    </form&gt;
{% endmodal %}

And you’re done! Now you have your own custom template tag. This pattern can be reused to create your own tag to repeat any frequently-used patterns in your HTML.

Of course there is much more you can do using custom block template tags. In my next post I’ll cover a few advanced topics including using keyword arguments, sub-tags, and including external templates. Stay tuned!

The final code for the modal template tag example, including some of the more advanced techniques, is available in this Gist.

Have you used block tags in your Django templates before? Let me know in the comments!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s