Proposal: allow simple_tag to parse @/- attributes

As it currently stands, passing attributes that contain @ and/or - to template tags implemented using @simple_tag raises an error.

Example:

@register.simple_tag
def greetings(*args, **kwargs):
    return 'greetings'

template:

{% greetings @click='' %}

error:

TemplateSyntaxError at /

Could not parse the remainder: '@click=''' from '@click='''

This errors regardless of the greetings implementation, since the decorator makes it fail upstream.

Alpine.js, htmx, and similar libraries are fairly popular these days. I believe it only makes sense to simplify the use of their attributes in cases that wouldn’t require a custom template.Node otherwise.

I think this is more likely an issue of nested quotes than of the ability to pass the tag.

All these following work:

{% greetings '@click=" " ' %}

{% greetings '@click=\' \' ' %}

{% greetings "@click=\" \" " %}

{% greetings "@click='  ' " %}

That seems like a different thing than what OP is asking for. He’s saying it would be useful to loosen the restriction of the syntax for keyword arguments in simple tags from <python_style_identifier>= to <printable_word>=.

I’m not sure I agree, but I can see why if you want to map JS libs that do this type of thing directly, and you have to mix @foo= with foo=.

Yes, I can see where that could be what’s being looked for, but given how those identifiers are (can be) used as a keyword arg, the limitation here is more one of python than Django - unless you want to completely change the semantics of that tag - in which case this becomes a much wider issue.

In Python you can pass any string as keyword argument though:

In [1]: def foo(**kwargs):
   ...:     print(kwargs)
   ...: 

In [2]: foo(**{"@hello!!": 1})
{'@hello!!': 1}

Yes, but you can’t accept it as a named parameter:
def foo(@hello!!): isn’t going to work, which means the function must access these by using kwargs, creating the situation where some of the template variables could be received directly, but some must not.

Sure. The limitations of Python still apply, but that is a separate question from what the template language should/could support I think.

I am +1 to this proposal, perhaps with an opt-in argument like @simple_tag(allow_all_attrs=True).

My heroicons package provides template tags made with simple_tag that allow setting hyphenated attributes, but it requires accepting attributes with _ and translating them to -

The HTML specification for attribute names allows pretty much any character:

Attribute names must consist of one or more characters other than controls, U+0020 SPACE, U+0022 ("), U+0027 ('), U+003E (>), U+002F (/), U+003D (=), and noncharacters.

I don’t think we can provide that level of flexibility without breaking parsing, but allowing some common punctuation (@, :, -) should be feasible…

1 Like

Thanks all for the feedback.

The question now is how to handle the function arguments.
I think the simpler, and probably most sensible, solution is to not support named arguments for attributes that contain python-disallowed argument characters. Basically only be able to access their values by kwargs["@hello"].
We could otherwise translate them somehow, as Adam describes. It might be too “magical” of a solution though, particularly for parameters with : and @.

1 Like

Hello everyone!

Thanks for the discussion so far. @giannisterzopoulos opened a ticket for this, and before accepting, I wanted to follow up and see if we can dig a bit deeper here.

There are two things I’m hoping to understand better:

  1. The actual use case: I acknowledge that alpine.js and htmx make heavy use of @, :, =, but I can’t see how this related to custom template tags. It would be really, really helpful to see a couple of concrete examples where this proposed parsing behavior makes a big difference. Are there specific template tags that would benefit from this? Seeing how this is being currently worked around it (or what we’d ideally like to write) could clarify things.

  2. The tradeoff: I’ve always seen tag arguments as Python-like named variables, so introducing characters like @, - or : into the syntax feels a bit like an anti-pattern to me. It makes me wonder: what’s the real value in supporting that syntax, and is it worth making that tradeoff in terms of clarity and consistency?

Looking forward to hearing more thoughts! If there’s a clearer use case and stronger demand, I’m definitely open to revisiting.

1 Like

Food for thought - I think this depends on whether, ultimately, Django template tags are seen as:

  • To help build HTML
  • To expose Python function within templates

In django-components, we’ve added support for the extra characters in the attribute names. Exactly because it’s common to:

  • Use @ and : prefixes in AlpineJS
  • Have HTML attributes that contain dashes, like data-testid

In our case we assume that our “components” render HTML, so it was logical to make these adjustments.

Agree with @adamchainz regarding breaking parsing - in case of django-components I ditched completely Django’s logic for parsing the template tag inputs.


Slightly off-topic, but I made a Rust-based template tag parser which is a superset of Django’s and which supports these extra features. Tho its output is an AST, so then there needs to be extra logic to convert the AST into actual args / kwargs. Happy to share what I know / what I got.

Hey everyone!

I initially noticed this limitation when using packages like django-avatar and lucide, where I tried to add some Alpine.js functionality like:
{% avatar user @click="alert('hey')" %} or
{% lucide "circle" @click="alert('hey')" %}

and would end up with the TemplateSyntaxError (similarly with the htmx ones). I was sometimes able to work around this by wrapping them in a parent element and use the special attributes there instead. This doesn’t always work though, depending on the expected behavior and the attributes at hand.

Some benefits that I see in addressing this in core:

  • Users (third-party package maintainers as well) won’t have to deal with translating the attributes case-by-case or come up with more hacky solutions.
  • They won’t have to resort to writing complicated Node subclasses either,
    and they can still benefit from the simplicity of the simple_tag.
  • I imagine that in many cases the passing of those attributes will be transparent for the tag authors: they don’t need to handle them explicitly within the tag code itself, but rather just pass them along to the rendered html.

I think this depends on whether, ultimately, Django template tags are seen as:

  • To help build HTML
  • To expose Python function within templates

As far as I can tell, simple_tag’s aim in particular is to help building HTML; judging by the steps it’s taking internally to produce valid HTML (usage of conditional_escape etc. - simple_tag docs)


Now I do see the concerns raised, especially the point on clarity and consistency.
It might be up to the implementation details to avoid any confusion though, and I realize that my proposal in the ticket was probably not up to that standard, or at least incomplete.
Any other ideas on how something like this could be implemented would be more than welcome.

Thank you all for the feedback.

Hi @giannisterzopoulos

Can you explain why @KenWhitesell’s suggestion wouldn’t work here?

{% avatar user '@click="alert('hey')"' %}

i.e. use an actual string, which is what you want passed to the template, right?

<div x-data @click="alert('hey')">Say hello!</div>

:thinking:

(Niggle looking at this: Those Alpine handlers grow to take any valid JavaScript. If we start down a road here, what possible (non-arbitrary) stopping point would there be? There’s a reason the DTL constrains in the way it does.)

Update:

This second example is better because you’ve got escaping issues with the nested quotes:

I look at that and my mind is scream Don’t do this! at me. :scream: But if I were going to, it’s a string I want to pass to the tag, so use a string, no? (The why not there is what I’m not seeing yet)

If it’s really necessary that the HTML attribute is not declared as a string parameter (but is parsed) the new https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#django.template.Library. would be my thought.

Hi @carltongibson

In lucide this would raise:

TemplateSyntaxError at /
'lucide' received too many positional arguments

and avatar results in wrong urls like https://www.gravatar.com/avatar/sama9r6spyhfcxkgrn58e3ybcgopunhe/?s=%40click%3D%22alert%28%27hey%27%29%22
because it maps what’s passed to the second argument (width - tag signature)

Support for passing those via kwargs would allow for bigger flexibility I think.

Yea I see where you’re coming from, the arbitrary JavaScript mostly affects the values themselves though no? which we still pass along as (escaped) strings. (The keys should be mostly defined sets that contain a certain amount of special chars)

Of course I could submit PRs/tickets to every package where I’d like the attrs in, so that at least they handle certain keys, but I’d really like it if we could come up with a way of dealing with this universally. :globe_with_meridians:

So the reason is, existing tags are using kwargs to populate generate HTML attributes directly from the key-value pairs?

I need to ponder about this more to know what I think about the suggestion here. I use these libraries (HTMX/Alpine) a lot and this isn’t an issue I’ve hit. (But maybe…)

Something I do quite often is to customise an existing tag by grabbing the underlying function and wrapping it to behave the way I want it to. (Maybe handy here, given that 6.0 is not until December, even if this is a good idea.)

1 Like

Is it just me or would this not throw the following error.

django.template.exceptions.TemplateSyntaxError: Could not parse the remainder: ‘hey’)"‘’ from ‘’@click=“alert(‘hey’)”‘’

While the following would work for custom tags that take *args (notice the necessary backslash), this feels a bit hacky.

{% button ‘@click=“alert(\‘hey\’)”’ %}Click me!{% endbutton %}

Also, something else to consider: While parsing “@”, “-” or “:” does not work for simple tags, or simple block tags, you can make it work with tags like the following:

class ButtonNode(Node):
    def __init__(self, nodelist, options):
        self.nodelist = nodelist
        self.options = options

    def render(self, context):
        # Resolve all option values
        resolved_options = {
            key: value.resolve(context) if hasattr(value, "resolve") else value for key, value in self.options.items()
        }

        # Get content from nodelist
        content = self.nodelist.render(context).strip()

        # Collect attributes for the button
        attrs = []

        # Process all other options as HTML attributes
        # This handles normal attributes, HTMX attributes, Alpine.js attributes, etc.
        for key, value in resolved_options.items():
            if key in ["put", "your", "other", "attributes", "here"]:
                continue  # Skip options that are used for styling

            if value is True:
                attrs.append(escape(key))
            elif value not in (False, None):
                attrs.append(f'{escape(key)}="{escape(str(value))}"')

        # Join attributes with spaces
        attrs_str = " ".join(attrs)

        return mark_safe(f'<button type="button" {attrs_str}>{content}</button>')


@register.tag("button")
def do_button(parser, token):
    bits = token.split_contents()[1:]
    options = {}

    for bit in bits:
        try:
            name, value = bit.split("=", 1)
            options[name] = parser.compile_filter(value)
        except ValueError:
            raise TemplateSyntaxError(f"button tag parameters must be in name=value format, got {bit}")

    nodelist = parser.parse(("endbutton",))
    parser.delete_first_token()

    return ButtonNode(nodelist, options)

This allows you to e.g. do:

{% button @click="console.log('Clicked!')" %}Click me!{% endbutton %}