Skip to content

Add a new map template tag that formats a list of items and returns them in a new list #178

Description

@meesterguyman

Code of Conduct

  • I agree to follow Django's Code of Conduct

Feature Description

In python, we can use the map function to perform an operation on each member of a list (or list-like object) and then return the results in a new list. For example, assuming a queryset of users:

format_func = lambda user: f'<div><b>{{ user.name }}</b> from {{ user.city }} ({{ user.age }}</div>'
formatted_users_list = map(format_func, users)

It would be helpful to be able to do this directly from within a template, so that, given a users queryset, we could derive formatted_users_list, which could then be passed on to another template tag or fed into another template for further processing. I would propose the following format:

{% map user in users as formatted_users_list %}
  <div><b>{{ user.name }}</b> from {{ user.city }} ({{ user.age }}</div>
{% endmap %}

In both of the above cases, formatted_users_list now holds formatted HTML for each user record. This list could then be passed to another template as follows:

{% include 'some/fixed/template.html' only_a_list_no_strings=formatted_users_list %}

Problem

If I have a list-like object, each of whose members need to be formatted in a particular way, and I want to display results directly on the page, a simple for loop will do the trick. If I want to capture the output as a string, a "simple_block_tag" will work. But if I want to format members and then pass results as a list to another template that requires a list rather than a string, then I need a template tag that operates on an iterable in template context and then injects the resulting list back into template context in a manner that is visible to subsequent tags / includes. At present, there is no straightforward way to do this.

Request or proposal

proposal

Additional Details

I've already built it. See below.

Implementation Suggestions

The following code is tested and working.

class MapNode(template.Node):
    def __init__(self, item_var, items_iterable, new_list_name, nodelist):
        self.item_var = item_var
        self.items_iterable = items_iterable
        self.new_list_name = new_list_name
        self.nodelist = nodelist

    def render(self, context):
        new_list = []
        for item in self.items_iterable.resolve(context, ignore_failures=True):
            context[self.item_var] = item
            new_list.append(self.nodelist.render(context))
        context[self.new_list_name] = new_list
        return ''

@register.tag('map')
def do_map(parser, token):
    """
    This tag is meant to emulate the python ``map`` function, accepting a list of X items, rendering them
    one at a time using the template fragment within the map block, and saving the result into a template
    variable, which can then be passed on to another template tag or template.

    For example, suppose we have a list of users and want to wrap each item in the list in HTML. We might
    do this in python as follows before passing it into template context::

        wrapped_users = map(lambda user: f"<div><b>{user.name}</b> from {user.city} ({user.age})</div>", users)

    Or we could do it directly inside a Django template as follows::

        {% map user in users as wrapped_users %}
            <div><b>{{ user.name }}</b> from {{ user.city }} ({{ user.age }})</div>
        {% endmap %}

    Either way, ``wrapped_users`` will now contain one HTML formatted item for each user in ``users``.

    The syntax here is similar to that of a for loop, but unlike the for loop, which renders output in place,
    an "as" clause is required here to indicate the name of the context variable in which the resulting list
    should be stored. This variable will then be injected into context for later use.
    """
    error = False
    try:
        _, item_var, in_kw, items_iterable, as_kw, new_list_name = token.split_contents()
        if in_kw != 'in' or as_kw != 'as':
            error = True
    except:
        error = True
    if error:
        raise TemplateSyntaxError("'map' tags take the form {%% map ITEM_VAR in ITEMS_ITERABLE"
                                  " as NEW_LIST_NAME %%}; you entered {%% %s %%}" % token.contents)
    nodelist = parser.parse(('endmap',))
    parser.delete_first_token()
    items_iterable = parser.compile_filter(items_iterable)
    return MapNode(item_var, items_iterable, new_list_name, nodelist)

Metadata

Metadata

Assignees

No one assigned

    Labels

    TemplatesThird Party PackageThis idea is suitable for the third party package ecosystem.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Idea

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions