Nunjucks templates are almost (but not quite) compatible with Jinja.
We wanted to find out if we could adapt existing templates written for Nunjucks so that they would render in Jinja.
Specifically, we wanted to understand what would be involved in making nhs-frontend Nunjucks templates cross-compatible with Jinja.
There are a number of small differences between Nunjucks 3 and Jinja that you need to be aware of if you want to support both.
Most of these are fairly minor changes to the templates.
In our whole design system, we found one small snippet we couldn't easily rewrite to be cross-compatible:
{% set isConditional = false %}
{% for item in params.items %}
{% if item.conditional.html %}
{% set isConditional = true %}
{% endif %}
{% endfor %}
This is due to differences in scoping rules, detailed below.
Currently, we need to add a custom filter to be able to handle this in both template engines (but we're still looking for another way)
We have a proof of concept here: nhsuk/nhsuk-frontend#1199
Nunjucks has experimental support for making templates more Jinja compatible, installJinjaCompat.
We found this did not help us. Consider the case where you have a value like this:
{
"items": []
}
In Nunjucks, you can call either var.items
or var["items"]
to retrieve the array.
In Jinja, a mapping is typically a python dictionary, so var.items
returns the dictionary's items()
method.
To be cross-compatible, our template needs to use var["items"]
.
This works because in Jinja/Python, objects have two ways of attaching values to objects: x.a
and x['a']
are different values.
Jinja variable access considers both, with the dictionary lookup taking precedence when you use the square bracket syntax.
installJinjaCompat
changes variable lookup so that all items
lookup return a fake items()
method.
This means there is no way to access the actual value in the object, so we would have to drop support for any objects with
items
keys (or any other keys that clash with python dictionary methods).
There is ongoing work on Nunjucks 4, which drastically improves the compatibility with Jinja. However, this is a way off yet. See Nunjucks 4 roadmap and release requirements.
Nunjucks 4 will add the following
- Missing core filters from Jinja
self
variablebreak
andcontinue
in loops- recursive for loops
{% block scoped %}
- make empty variables falsey
- fixes to
{% call %}
andcaller
As described above, you need to be careful when using dotted variable lookups.
For mappings, avoid python dictionary methods:
clear
copy
get
items
keys
pop
popitems
setdefault
update
values
The solution is to use square bracket syntax instead.
Bad (clashes with dict.items()
in Jinja):
{% for item in params.items %}
Good:
{% set items = params['items'] if 'items' in params else [] %}
{% for item in items %}
Jinja's +
operator does not coerce its arguments to strings. Nunjucks does a lot of coercing to string (following Javascript rules.)
To concatenate a string you should use the (undocumented in Nunjucks) ~
operator in place of +
.
Mapping literals in Jinja need to have their keys quoted. If you forget to do this, the keys will be interpreted as variables when rendering in Jinja and will evaluate to undefined
.
Bad:
{{ hint({
id: itemHintId,
classes: 'nhsuk-checkboxes__hint',
attributes: item.hint.attributes,
html: item.hint.html,
text: item.hint.text
}) }}
Good:
{{ hint({
"id": itemHintId,
"classes": 'nhsuk-checkboxes__hint',
"attributes": item.hint.attributes,
"html": item.hint.html,
"text": item.hint.text
}) }}
In Jinja you cannot modify a variable in a loop and reference it outside the loop, due to its scoping behaviour.
Bad:
{% set isConditional = false %}
{% for item in items %}
{% if item.conditional %}
{% set isConditional = true %}
{% endif %}
{% endfor %}
On the other hand, in Nunjucks, you can't modify attributes of objects in set
statements.
Also bad:
{% set ns.isConditional = false %}
{% for item in items %}
{% if item.conditional %}
{% set ns.isConditional = true %}
{% endif %}
{% endfor %}
In some cases, this can be avoided by using filters instead of a loop.
Good:
{% set isConditional = items | selectattr("conditional") | list | length > 0 %}
Nunjucks 3 does not support all of the filters in Jinja, and adds some of its own. To support both, you need to stick to the common subset.
In practice, this is not a major issue, except that map()
, if available, could be used to replace problematic for loops that don't function in Jinja (see the comments on scoping above).
The below list may not be 100% accurate, as not everything is documented in Nunjucks.
Filter | Jinja | Nunjucks |
---|---|---|
abs() | ✅ | ✅ |
attr() | ✅ | ❌ |
batch() | ✅ | ✅ |
capitalize() | ✅ | ✅ |
center() | ✅ | ✅ |
default() | ✅ | ✅ |
dictsort() | ✅ | ✅ |
dump() | ❌ | ✅ |
escape() | ✅ | ✅ |
filesizeformat() | ✅ | ❌ |
first() | ✅ | ✅ |
float() | ✅ | ✅ |
forceescape() | ❌ | ✅ |
format() | ✅ | ❌ |
groupby() | ✅ | ✅ |
indent() | ✅ | ✅ |
int() | ✅ | ✅ |
items() | ✅ | ❌ |
join() | ✅ | ✅ |
last() | ✅ | ✅ |
length() | ✅ | ✅ |
list() | ✅ | ✅ |
lower() | ✅ | ✅ |
nl2br() | ❌ | ✅ |
map() | ✅ | ❌ |
max() | ✅ | ❌ |
min() | ✅ | ❌ |
random() | ✅ | ✅ |
reject() | ✅ | ✅ |
rejectattr() | ✅ | ✅ |
replace() | ✅ | ✅ |
round() | ✅ | ✅ |
safe() | ✅ | ✅ |
selectattr() | ✅ | only single-argument form |
slice() | ✅ | ✅ |
sort() | ✅ | ✅ |
string() | ✅ | ✅ |
striptags() | ✅ | ✅ |
sum() | ✅ | ✅ |
title() | ✅ | ✅ |
trim() | ✅ | ✅ |
tojson() | ✅ | ❌ |
truncate() | ✅ | ✅ |
upper() | ✅ | ✅ |
urlize() | ✅ | ✅ |
urlencode() | ✅ | ✅ |
wordcount() | ✅ | ✅ |
wordwrap() | ✅ | ❌ |
xmlattr() | ✅ | ❌ |
JS methods like includes
cannot be used in Jinja.
Instead of includes
use the in
keyword. Instead of length
use the length filter.
Bad:
{{ items.length }}
Good:
{{ items | length }}
Strict equality operators (===
and !===
), do not exist in Jinja.
The is
keyword (e.g. foo is True
), works in Jinja but does not work in Nunjucks.
If the type of a value is unknown, == true
can be safely used in place of === true
. This expression is true for a boolean true value or the number 1
, and false for any other values, including strings.
== false
is weaker than === false
and should be avoided. It is true for false
, null
, and undefined
values, but values of 0
, ""
, []
or {}
will yield different results depending on whether you are using Nunjucks or Jinja.
In Jinja the behaviour of undefined depends on configuration. For compatibility with Nunjucks, configured undefined to be ChainableUndefined
. For example, in Flask:
from jinja2 import ChainableUndefined, ChoiceLoader, FileSystemLoader
app.jinja_options = {
"undefined": ChainableUndefined, # This is needed to prevent jinja from throwing an error when chained parameters are undefined
"loader": ChoiceLoader(
[FileSystemLoader(ROOT / "packages"), FileSystemLoader(ROOT / "app")]
),
}
Relative imports don't work in Jinja. It's possible to customise this behaviour via subclassing the Jinja environment, but generally it's easier to use absolute paths relative to a root directory, e.g. packages
.
Note: This pattern is not actually present in our codebase, but could still cause issues.
In Nunjucks, objects are considered iterable. In Jinja, to iterate over an object you need to explicitly add .items()
or use the items
filter.
The items
filter doesn't exist in Nunjucks, but you can use the dictsort
filter as a workaround that works identically in both template engines.
Bad:
{% for name, value in foo %}
{{name}}={{value}}
{% endfor %}
OK:
{% for name, value in foo | dictsort %}
{{name}}={{value}}
{% endfor %}