In Django, there are two ways to extend the template language — tags and filters. Tags are named tokens that insert HTML, executing logic behind the scenes. For instance, an if tag and a for tag will conditionally produce content or iterate over a list objects respectfully. The if tag in Django takes an argument — the condition to evaluate.
Filters, on the other hand, are different from tags — they don't spit out HTML markup or alter the overall template logic. Instead, filters modify existing values in the template context for the sake of presentation. For example, Django ships with a fileformat filter that'll display the argument, expressed in bytes, as a more readable representation. This type of functionality is best encapsulated inside a filter because we're simply modifying the presentation of a single value.
I used to find myself grappling over whether something should be implemented as a tag or as a filter. On the one hand, custom tags capacious changes — chunks of HTML or logic for manipulating the template. On the other hand, filters are concise. They take input and modify it. Filters exchange one piece of data for another. Django provides the machinery to do this, to slice up monolithic templates into reusable, independent components. My inclination is that custom template tags can often be avoided in favor of included templates and filters.
Modular templates
If every page in our application were rendered using a single gargantuan template, we wouldn't be burdened with choosing between custom tags and custom filters. We'd have a number of other problems, no doubt, but I digress. Instead, Django treats template files much like Python treats modules. Django templates are modular. Why write one large template, duplicated for individual views? That approach doesn't follow the DRY principle, so we need a mechanism that'll allow us to treat our templates as components.
A component can have sub-components. This abstraction works well in the Django template architecture because components are made up of smaller components. They can be decomposed and reconstructed, using smaller, loosely-coupled components. In Django-speak, this means that we can start with a template that represents the entire page. Logically, based on the application for which this template was designed, we can map out sections of this template that are likely to appear on all pages. Things such as navigation, footer, and so forth.
These sub-components need to fill slots. These slots are called blocks, in Django. Typically, the master template is extended by descendant templates whose job is to define what fills these slots. Now we're starting to move down the composite template structure. Beyond blocks, we need something to fill them with.
Eventually, fine-grained HTML markup is generated. Some of it conditional, some of it iterative, and some using variables from the template context. But even at this level, where we're filling in blocks, there are smaller pieces still. Pieces that don't necessarily belong to a specific block. Down yet another level, we're producing markup that might even appear twice on the same page — in two distinct blocks.
As the user interface of your Django project evolves and forms it's shape, you'll begin to notice these smaller chunks — those that might be of interest to multifarious blocks. The main blocks, the logical regions of the master template are easy to grasp early on in development. It's the smaller template components — the unbound HTML markup — that are more difficult to identify. These low-level template components must take into consideration both custom template tags and custom template filters.
Sharing data
The tools that the Django template system gives developers enables the sharing of data. Sharing between what exactly? You extend the template language because you want to reuse those elements — custom tags and filters — across templates in your application. But these new elements don't generate new data. They're not storing application data. Rather, the data new template elements share is transformed data. They take input and alter what the template ultimately sends to the browser.
Let's say you've got an application that lists events of interest to the user when they first login. These are probably queried from the database and rendered inside a for tag. But maybe the event abstraction isn't limited to a user. Maybe there are different event types that pertain to other abstractions in the application and aren't exclusive to the user. Here, we might want to reuse a much of the same logic that rendered events on the homepage. We're only changing the query.
We could follow the DRY principle here and implement a custom eventlist tag. This tag would accept some type of flag, indicating the query to execute in order to render the appropriate event list. Using this tag in our templates is then quite straightforward — {% eventlist 'home' %} or {% eventlist 'updates' %}. This tag is available in all templates — an easy way to share data across Django templates.
Our tag could even take care of rendering the corresponding HTML markup. We'd simply register these eventlist tag as an inclusion tag. We've now got our own template, isolated from other templates in our user interface dedicated to rendering event lists.
There are, however, a couple problems with this approach. One, the template tag itself is responsible for executing the query. This means that our view context passed to the template during rendering cannot alter the output of the event list. So if we want to use generic views that'll generate a list of events to render, we're stuck. The eventlist tag is expecting an arbitrary flag. We've given the Django template the added responsibility of performing database queries — which isn't a good thing. We could alter our template tag so that it accepts a query set from the template context. This way the view is still responsible for retrieving objects. This is a good thing. However, this leads to another problem. One where we're violating the DRY principle.
Including and filtering
Tags are good because they're a concise way of sharing rendered HTML with other templates. Our eventlist tag does exactly that. But they're also a gateway into bad Django practice. Defining your own template tags means writing some Python code. Which gives us direct access to models. Which means we can query them. Not good. Not good as far as templates go because templates should be able to render anything I give them. If I want to pass to my tag context, say, a list of Python objects that merely emulate one of my models, it should be able to handle that. This isn't true if the template tags we define are going directly to the database for objects to render.
As for remedying the problem, we mentioned passing a query set, a list, something, to the template tag as an argument. This somewhat solves the problem because now eventlist is polymorphic to an extent. It doesn't care what objects it gets as long as they look like event objects.
Recall, however, that we're defining eventlist as an inclusion tag. This means a template dedicated to the list of events will be rendered whenever the tag is used. Keeping the HTML in the template is a smart idea — another win for us. The eventlist tag is now simply passing the list of events to render. Perfect. Except, what doesn't the template tag do now? It takes a list of objects and passes them into another template context. It does exactly what the Django include tag does.
The include tag takes a smaller template component — in our case a list of rendered event objects — and plugs it into a larger component. This sounds like an ideal candidate for our application. Our views can now control the context — the list of events that get displayed in any given block. And we're not repeating ourselves — we've got a template component that can be injected anywhere, and no need to define a custom template tag.
How about the template itself — the one we're including all over the site? Are there any drawbacks to simply including it somewhere else without using our own tag? One challenge with making template components generic enough to be used anywhere is that they need to support a variety of scenarios. What happens when the event object is in this state? When it doesn't have a title attribute? How do I handle CSS classes for events on this page?
Trying to handle all this in a single template that's used everywhere leads to messy template code. In a hurry, you've got if statements inside HTML attributes and deep nesting levels elsewhere. This is where defining custom template filters can alleviate some of these challenges. Typical Django filters will take an input value and return a slightly different version of it — like a formatted date. But we're not limited to only modifying the display of data. We can return different values entirely. Values used for the sole purpose of template rendering.
Template filter definitions are small. We might have an input value, we do a few checks, and we return a different value. Think of filters as dynamic template context modifiers — modifiers that can be shared throughout the application. Filters can also justify not having to define your own custom tags, ultimately leading to cleaner template code.
No comments :
Post a Comment