CSS class helper for Jinja
Posted on Thu 22 October 2015 in Code
One of the great uses for a templating engine such as Jinja is reducing the clutter
in our HTML source files. Even with the steady advances in the CSS1 standards, various div.wrapper
, div.container
,
div.content
, and other presentational elements are still a common fact of life.
Unless you’re one of the cool kids who use Web Components with
Polymer, your main option for abstracting this boilerplate away
is defining some more general template macros.
As with any kind of abstraction, it’s crucial to balance broad applicability with a potential for finely-tuned customization. In the case of wrapping HTML snippets in Jinja macros, an easy way to maintain flexibility is to allow the caller to specify some crucial attributes of the root element:
{#
Renders the markup for a <button> from Twitter Bootstrap.
#}
{% macro button(text=none, style='default', type='button', class='') %}
<button type="{{ type }}"
class="btn btn-{{ style }}{% if class %} {{ class }}{% endif %}"
{{ kwargs|xmlattr }}>
{{ text }}
</button>
{% endmacro %}
An element id
would be a good example, and in the above macro it’s handled implicility
thanks to the {{ kwargs|xmlattr }}
stanza.
class
, however, is not that simple, because a macro like that usually needs to supply some CSS classes of its own.
The operation of combining them with additional ones, passed by the caller, is awfully textual and error-prone.
It’s easy, for example, to forget about the crucial space and run two class names together.
As if CSS wasn’t difficult enough to debug already!
Let them have list
The root cause for any of those problems is working at a level that’s too low for the task.
The value for a class
attribute may be encoded as string, but it’s fundamentally a list of tokens.
In the modern DOM API, for example, it is represented
as exactly that: a DOMTokenList
.
I’ve found it helpful to replicate a similar mechanism in Jinja templates. The result is a ClassList
wrapper
whose code I quote here in full:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | from collections import Iterable, MutableSet
class ClassList(MutableSet):
"""Data structure for holding, and ultimately returning as a single string,
a set of identifiers that should be managed like CSS classes.
"""
def __init__(self, arg=None):
"""Constructor.
:param arg: A single class name or an iterable thereof.
"""
if isinstance(arg, basestring):
classes = arg.split()
elif isinstance(arg, Iterable):
classes = arg
elif arg is not None:
raise TypeError(
"expected a string or string iterable, got %r" % type(arg))
self.classes = set(filter(None, classes))
def __contains__(self, class_):
return class_ in self.classes
def __iter__(self):
return iter(self.classes)
def __len__(self):
return len(self.classes)
def add(self, *classes):
for class_ in classes:
self.classes.add(class_)
def discard(self, *classes):
for class_ in classes:
self.classes.discard(class_)
def __str__(self):
return " ".join(sorted(self.classes))
def __html__(self):
return 'class="%s"' % self if self else ""
|
To make it work with Flask, adorn the class with
Flask.template_global
decorator:
from myflaskapp import app
@app.template_global('classlist')
class ClassList(MutableSet):
# ...
Otherwise, if you’re working with a raw Jinja Environment
,
simply add ClassList
to its global namespace directly:
jinja_env.globals['classlist'] = ClassList
In either case, I recommend following the apparent Jinja convention of naming template symbols
as lowercasewithoutspaces
.
Becoming classy
Usage of this new classlist
helper is relatively straightforward. Since it accepts both a space-separated string
or an iterable of CSS class names, a macro can wrap anything the caller would pass it as a value for class
attribute:
{% macro button(text=none, style='default', type='button', class='') %}
{% set classes = classlist(class) %}
{# ... #}
The classlist
is capable of producing the complete class
attribute syntax (i.e. class="..."
), or omit it entirely
if it would be empty. All we need to do is evaluate it using the normal Jinja expression syntax:
<button type="{{ type }}" {{ classes }} {{ kwargs|xmlattr }}>
Before that, though, we may want to include some additional classes that are specific to our macro.
The button
macro, for example, needs to add the Bootstrap-specific
btn
and btn-$STYLE
classes to the <button>
element it produces2:
{% do classes.add('btn', 'btn-' + style) %}
After executing this statement, the final class
attribute contains both the caller-provided classes,
as well those two that were added explicitly.
-
Believe it or not, we can finally center the content vertically without much of an issue! ↩
-
The
{% do %}
block in Jinja allows to execute statements (such as function calls) without evaluating values they return. It is not exposed by default, but adding a standardjinja2.ext.do
extension to theEnvironment
makes it available. ↩