Thinking this idea out to its fullest extent, brings up five possible forms:
- Always output content, assignment via
as varname
is not allowed. The built in{% now %}
is an example. - Always output content, assignment via
as varname
is optional. The built in{% cycle %}
is an example. - Some times output, some times assign, depending upon the presence of
as varname
. The built in{% url %}
is an example. - Always assign,
as varname
is optional. There is no built in example. - Always assign,
as varname
is required. The built in{% regroup %}
is an example.
Currently, the template tag decorators for custom tags only support two forms:
form #1 via simple_tag
and form #5 via assignment_tag
. This proposal
is about extending the Django template tag decorators to expose the third and
fourth forms. A ticket exists for this proposal: Ticket #18651.
The third form is already in use in the Django built in template tags - namely
the {% url %}
tag. I have come across multiple situations similar to the
{% url %}
tag where optional assignment would make things much simpler.
Consider the following use case:
<a href="{% url "foo" %}">
Or:
{% url "foo" as foo_url %}
<h1><a href="{{ foo_url }}">Title</a></h1>
<p>Body</p>
<a href="{{ foo_url }}">Read more...</a>
The fourth form is not in use by Django. It is much less common, but I have seen instances of it in the wild. Consider the following template tag:
{% get_comments_for foo %}
{% for comment in comments %}
{{ comment }}
{% endfor %}
But if you need to work on two lists at once:
{% get_comments_for foo as foo_comment %}
{% for comment in foo_comments %}
{{ comment }}
{% endfor %}
{% get_comments_for bar as bar_comment %}
{% for comment in bar_comments %}
{{ comment }}
{% endfor %}
Extending the template tag decorators to support the second form is not part of
this proposal. The way {% cycle .. as varname %}
works is surprising and
strange.
There are three proposed solutions to this:
- Add new decorators to support the new forms
- Extend the
assignment_tag
decorator with extra kwargs, to make assignment optional or to provide a default name. - Extend the
simple_tag
decorator with extra kwargs, to make output optional.
# Form 3
@register.optional_assignment_tag():
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag_with_default_name('comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
New decorators are very easy to write. They do not have any surprising behaviour, and they do exactly what they say on the tin.
It is a whole new decorator that has to be maintained, documented, tested, and
worked with. Most of the code will be duplicated between this decorator and the
assignment_tag
decorator. This is not very DRY.
The names are overly verbose, but making them more concise will make then less explicit.
# Form 3
@register.assignment_tag(optional_assignment=True):
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag(default_name='comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
Modifying the assignment_tag
is a simple change. It involves two extra kwarg,
and some small modifications to the assignment_tag
decorator. The assignment
tag is still used mostly for assignment. It prevents code duplication, and
keeps things DRY
You can see an example implementation here: https://github.com/maelstrom/django/commits/ticket-18651
The assignment_tag
now does two things: assigning its result to a variable in
the context as normal, and outputting like simple_tag
does. This is
surprising and inconsistent with the naming of the decorator.
The two new kwargs are mutually exclusive. Using them both at the same time would be an error.
# Form 3
@register.simple_tag(can_assign=True):
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag(can_assign=True, default_name='comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
simple_tag
can now act like some of the built in tags, like {% url %}
. It
is easy to comprehend. It is less surprising than modifying assignment_tag
to
not always assign things. It is very explicit in what it is doing.
You can see an example implementation here: https://github.com/maelstrom/django/commits/ticket-18651-v2
This requires a more extensive code rewrite, as simple_tag
is not set up to
work like this.
assignment_tag
is also more or less obsolete now, as simple_tag
can do
everything that assignment_tag
can.
simple_tag
is slowly getting less and less simple as more functionality is
heaped upon it.
When using both can_assign
and assignment_tag
, a simple_tag
will never
output anything. This is inconsistent with the rest of its behaviour.