Support more complex syntax with templatetags
The following gives a long error, and reading the source code it looks like you don't yet support {% ... %} within the component. It would be very nice to be able to use that syntax. It would also bring it to feature parity with django-cotton in that regard as that syntax is not an issue there. (though syntax obviously looks different there).
{% component "text_field"
type="{{ widget.type }}"
name="{{ widget.name }}"
{% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}
%}
{% endcomponent %}
I'm actually in the middle of working on these template kind of features, so this comes in the right time!
Using {% ... %} inside component input
Currently how it works is that you can put {% ... %} wrapped in string quotes to have it evaluate as a nested template, like in your case of the type and name kwargs.
But django-components doesn't support the kind of "meta-programming" where you could use {% ... %} to construct the inputs.
Everything following {% component is treated as args and kwargs, the same way as Python functions.
So
{% component "text_field"
type="{{ widget.type }}"
name="{{ widget.name }}"`
%}
Is the same as
component("text_field", type=widget.type, name=widget.name)
Hence why using {% if %} to construct the kwargs is problematic, because it would be like if you did this in Python:
component(
"text_field",
type=widget.type,
name=widget.name,
if widget.value:
value=widget.value
)
The benefit of not allowing {% if %} inside {% component %} is that it will pave the way for linter / syntax highlighter, which would be otherwise hard or impossible to do.
Jinja-like Python expressions in templates
Instead, I'd like to go in a different direction and introduce sandboxed Python expressions with the same safety measures like Jinja has (see https://github.com/django-components/django-components/issues/1178). These would be defined by parentheses, and could really clean up the component inputs:
E.g. your example could be rewritten as:
{% component "text_field"
type=widget.type
name=widget.name
value=( str(widget.value) if widget.value else None )
%}
{% endcomponent %}
CC @EmilStenstrom Initially I was thinking of this feature for v2, but even simple logic like not x is still cumbersome in Django templates, so I'd like to introduce it to v1 already.
I'm about half-way with implementing this in Rust.
The way it works is that:
- We receive a Python expression as a string, e.g.
1 + a.b() - Convert it to AST
- We check which AST nodes are used, and raise error on forbidden syntaxes. That way we disallow the use of things like
import, etc. - Then, how Jinja works is that it checks at runtime whether attribute access (
a.b), subscript access (a[b]), or function call (a(b)), whether they are doing something dangerous. - The way we achieve that is that we convert these into function calls of our functions
a->variable("a", context)a.b->access(a, b)a[b]->subscript(a, b)a(b)->call(a, b)
- We define the implementations of
variable(),access(),subscript(),call(), where we check for dangerouns stuff like:- is
accesstrying to access private or dunder methods / attributes? - the function we want to call, is it safe?
- When generating ranges, is it sane list size?
- is
Replacing django/forms/widgets/attrs.html
@jonathan-s Lastly I looked into the {% include "django/forms/widgets/attrs.html" %}. That code looks like this:
{% for name, value in widget.attrs.items %}
{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}
{% endfor %}
So what this does is that it formats the attributes. So in your example, you format the attributes before you pass them to the "text_field" component.
Instead, in django-components, you would do things a bit differently (more similarly to React or Vue):
-
Pass data to "text_field" component as kwargs:
{% component "text_field" type=widget.type name=widget.name value=( str(widget.value) if widget.value else None ) attrs=widget.attrs %} {% endcomponent %} -
Let "text_field" decide how to format the attributes
Inside the "text_field" component format the attributes with
{% html_attrs %}:class TextField(Component): def get_template_data(self, args, kwargs, slots, context): return { "attrs": kwargs["attrs"], } template = """ <input {% html_attrs attrs %}> """
@JuroOravec Thanks for considering this and thanks for typing out a possible solution! I ended up writing out template filter and doing the following. Do note that attrs that we want is not actually widget.attrs there are some subtle manipulations.
{% with attrs=widget|to_attrs %}
{% component "text_field"
type="{{ widget.type }}"
name="{{ widget.name }}"
...attrs
%}
{% endcomponent %}
{% endwith %}
I think this pattern value=( str(widget.value) if widget.value else None ) seems like a reasonable solution. Though documentation is key here. I think part of this documentation should be that when the developer tries using template tags with if statements it should guide the developer to use the correct pattern.
Btw small comment but you're not using the attrs object anywhere else, you could drop the {% with %}:
{% component "text_field"
type="{{ widget.type }}"
name="{{ widget.name }}"
...attrs|to_attrs
%}
{% endcomponent %}
Though documentation is key here. I think part of this documentation should be that when the developer tries using template tags with if statements it should guide the developer to use the correct pattern.
Hm, interesting - we could do this also on the code level 👀
I wrote the parser on top of Rust's Pest parser, where one defines grammar rules for parsing the text. I want to reuse this parser for linting, so that we can report issues with the component inputs. Right now the parser throws and aborts at the first error it comes across. However, for it to work as a linter, I want it to find ALL errors, not just the first one.
Well, and I got a suggestion from GPT that a common way to handle this is by including the common errors in the language grammar. E.g. right now the grammar may look like below. So we could extend it to recognize also the "{% if ... %} inside another {% ... %}" pattern and raise error and suggest a fix.
// The full tag is a sequence of attributes
// E.g. `{% slot key=val key2=val2 %}`
tag_wrapper = { SOI ~ (django_tag | html_tag) ~ EOI }
django_tag = { "{%" ~ tag_content ~ "%}" }
// The contents of a tag, without the delimiters
tag_content = ${
spacing* // Optional leading whitespace/comments
~ tag_name // The tag name must come first, MAY be preceded by whitespace
~ (spacing+ ~ attribute)* // Then zero or more attributes, MUST be separated by whitespace/comments
~ spacing* // Optional trailing whitespace/comments
~ self_closing_slash? // Optional self-closing slash
~ spacing* // More optional trailing whitespace
}
Another note here, another common pattern that will occur is to use the following as a value for a keyword argument in component.
{% url 'some-view' my_kwarg=variable %}
Would be good to have documentation on how you should work around this in a good way as well if we disallow {% within the keyword arguments for component.
Or I could be mistaken, that the above actually works right now.
Would be good to have documentation on how you should work around this in a good way as well if we disallow {% within the keyword arguments for component.
To clarify, how it works right now is that you can use {{ ... }} and {% ... %} as (kw)arguments, but you just need to wrap them in quotes.
So something like the example below is possible already. It's just that the {% ... %} template tags currently need to be wrapped in string quotes:
{% component "button"
href="{% url 'some-view' my_kwarg=variable %}"
name="{{ widget.name }}"
%}
IMO, it would be nicer if we could drop the quotes, so just
href={% url 'some-view' my_kwarg=variable %}
name={{ widget.name }}
BUT:
-
We allow also literal dictionaries, and it'd be hard to distinguish between nested literal dicts and
{{ .. }}, e.g.items={"person_a": {"name": "John"}} ^^No such problem with
{% .. %}. -
There's actually no point in passing
{{ ... }}to (kw)arguments, you can just drop the curly brackets, soname="{{ widget.name }}"becomes
name=widget.name -
So this option to omit the enclosing string quotes would apply only to
{% ... %}tags✅ valid href={% url 'some-view' my_kwarg=variable %} ❌ not valid name={{ widget.name }}
And then the question is - would it not be confusing if only {% ... %} were treated like this?
On the other hand, {% ... %} is a strong pattern in Django templates. So I don't see an issue with treating them as first class syntax.
@EmilStenstrom What's your thoughts?
That makes sense! Thanks for the explanation. I think as long as you can use {% in quotes that alleviates a heap of problems.
href={% url 'some-view' my_kwarg=variable %}
Since the above would effectively be
href="{% url 'some-view' my_kwarg=variable %}"
I'm not sure it would be worth the effort to spend too much time into it. But having if statements one way or another working would be worth the effort.