Feature proposal: Simple block tag

Django’s template system allows creating custom tags, but they don’t allow taking content:

{% mysimpletag foo="bar" %}

However there are plenty of use-cases where it’d be useful to also collect content along with the tag, turning it into a “block” tag:

{% mysimpletag foo="bar" %}
  This content gets captured too
{% endmysimpletag %}

This currently requires using complex internals of the template system (some documented, some not) to achieve. The cache tag is a good example of this kind of “simple” block, which requires some parser internals.

I propose adding a @register.simple_block method to allow easy registering of custom block tags. The API would be similar to simple_tag, however with a required content argument containing the (rendered) template content:

@register.simple_block(takes_context=True)
def mysimpletag(context: Context, content: str, foo: str) -> str:
    return f"foo = {foo}"

I raised a ticket about this, but was pointed here as a better way of collecting interest. I already have a working demo of this working.

1 Like

I’m +1 in the general direction here. (There’s are any number of historical threads asking for related ideas.)

FWIW, assuming some as integration, I think it’s #6378 (Capture arbitrary output as a template variable) – Django revisited :sweat_smile:

1 Like

You can take a look at https://github.com/rails-inspire-django/django-viewcomponent

With the slot feature, you can pass huge HTML to the component and then do some operation.

It has helped me solved similar problems in some projects.

Yes… — that’s a nice package @michael-yin.

There are several in this space. The danger is we get into a never ending discussion about which features to add. (This happens a lot :sweat_smile:)

Simple, to the point, covers the 80% case is probably enough for an addition to Django itself here, leaving space in the ecosystem for folks to explore more advanced options.

+1 from me. I have missed this in the past.

Presumably it will allow both positional and keyword arguments, like simple_tag. They would need to be checked for collision with the name content.

I suggest the name simple_block_tag to retain the naming scheme of simple_tag and inclusion_tag.

Where?

Indeed, this would be called “simple” for that reason.

Absolutely! We can reuse much of the implementation of simple_tag to keep many of the same semantics. Validating the overlap of the content argument is something I’d missed, but assuming simple_tag does the same, I should be able to share some of the implementation.

It was in a private repo when I opened the ticket, but I recently pushed up a branch to start integrating it with Django core. It’s still very rough, and missing “important” things like tests, but the basics are there: GitHub - RealOrangeOne/django at 35535-simple-block-tag. Hoping to get around to finishing it off and writing the related in the next week or so to get a draft PR up.

2 Likes

I’m late but also +1.

It might be useful to document an example use-case for it, so people have an idea when to use it.

et voilà: Fixed #35535 -- Add simple block tag by RealOrangeOne · Pull Request #18343 · django/django · GitHub

I’m late too, but maybe we could add an inclusion_block_tag. I’m not an expert but this is what I have (based on the simple_block_tag):

from functools import wraps
from inspect import getfullargspec, unwrap

from django.template import Library
from django.template.exceptions import TemplateSyntaxError
from django.template.library import InclusionNode, parse_bits

class ExtendedLibrary(Library):
    def inclusion_block_tag(self, filename, func=None, takes_context=None, name=None):
        def dec(func):
            (
                params,
                varargs,
                varkw,
                defaults,
                kwonly,
                kwonly_defaults,
                _,
            ) = getfullargspec(unwrap(func))
            function_name = name or func.__name__

            @wraps(func)
            def compile_func(parser, token):
                tag_params = params.copy()

                if takes_context:
                    if len(tag_params) >= 2 and tag_params[1] == "content":
                        del tag_params[1]
                    else:
                        raise TemplateSyntaxError(
                            f"{function_name!r} is decorated with takes_context=True so"
                            " it must have a first argument of 'context' and a second "
                            "argument of 'content'"
                        )
                elif tag_params and tag_params[0] == "content":
                    del tag_params[0]
                else:
                    raise TemplateSyntaxError(f"'{function_name}' must have a first argument of 'content'")

                bits = token.split_contents()[1:]

                nodelist = parser.parse((f"end{function_name}", f"end_{function_name}"))
                parser.delete_first_token()

                args, kwargs = parse_bits(
                    parser,
                    bits,
                    tag_params,
                    varargs,
                    varkw,
                    defaults,
                    kwonly,
                    kwonly_defaults,
                    takes_context,
                    function_name,
                )
                return InclusionBlockNode(filename, nodelist, func, takes_context, args, kwargs)

            self.tag(function_name, compile_func)
            return func

        return dec


class InclusionBlockNode(InclusionNode):
    def __init__(self, filename, nodelist, *args, **kwargs):
        super().__init__(filename=filename, *args, **kwargs)
        self.nodelist = nodelist

    def get_resolved_arguments(self, context):
        resolved_args, resolved_kwargs = super().get_resolved_arguments(context)

        # Restore the "content" argument.
        # It will move depending on whether takes_context was passed.
        resolved_args.insert(1 if self.takes_context else 0, self.nodelist.render(context))

        return resolved_args, resolved_kwargs

Edit: formatting