Description
Problem
I'm updating one of my projects from v0.73 to v0.82. Features like the html_attrs
tag, prop:key=value
syntax and provide/inject cleaned up the code really nicely.
However, there are still a few use cases where I need to define HTML attributes in Python (instead of keeping them in the HTML), to work around the limitations of the library. This adds unnecesary mental overhead as I need to trace where the HTML attributes belong.
Consider the example below, where:
-
I am passing
attrs
(dict with HTML attrs) to child componenttab
. I want to add some extra HTML attrs to it (@click=...
). Ideally the@click=...
HTML attribute could live in the template, directly on thetab
component. But because I needed to merge my HTML attribute with the user-givenattrs
, I had to define it in the Python section. -
I'm passing in a list of TabItem objects. These have
disabled
attribute. However,input_form
component acceptseditable
, which in this case is the negation ofdisabled
. I cannot doeditable=not disabled
inside the template, so I had to convert the list ofTabItems
to a list of dictionaries, so I could define an extra keyreadonly
.
class TabItem(NamedTuple):
content: str
disabled: bool = False
@component.register("tabs")
class Tabs(Component):
template = """
<div>
{% for tab in tabs %}
{% component "tab"
content=tab.content
disabled=tab.disabled
attrs=attrs
%}{% endcomponent %}
{% component "input_form"
editable=tab.editable
%}{% endcomponent %}
{% endfor %}
</div>
"""
def get_context_data(
self,
*_args,
tabs: list[TabItem],
attrs: dict | None = None,
):
final_attrs = {
**(attrs or {}),
"@click": "(evt) => alert(evt.target.data)"
}
final_tabs = []
for tab in tabs:
final_tabs.append({
"content": tab.content,
"disabled": tab.disabled,
"editable": not tab.disabled,
})
return {
"tabs": final_tabs,
"attrs": final_attrs,
}
Solution
❌ Custom filters
While custom filters could be enough for the negation:
editable=tab.disabled|not
It's already insufficient for ternary (if/else). Django has the built-in yesno
filter, but the filter works only with strings. I cannot use yesno
to decide between two objects.
This works:
editable=tab.disabled|yesno:"yes,no"
But I cannot achieve this:
editable=tab.disabled|yesno:this_if_true,that_if_false
One custom filter I can think of that could work is if I defined a filter that runs a function:
@register.filter("call")
def call_fn(value, fn):
return fn(value)
But here the limitation is that I could pass only a single value to it.
So while it could work with predefined True/False values:
def this_that_ternary(predicate):
return this_if_true if predicate else that_if_false
editable=tab.disabled|call:this_that_ternary
I couldn't pass the True/False values on the spot:
editable=tab.disabled|call:ternary(this_if_true, that_if_false)
✅ Custom tags
The upside of tags is that you can pass in an arbirary number of arguments, and you can capture the output with as var
syntax:
@register.simple_tag
def ternary(predicate, val_if_true, val_if_false):
return val_if_true if predicate else val_if_false
{% ternary tab.disabled this_if_true that_if_false as tab_editable %}
{% component "input_form"
editable=tab_editable
%}{% endcomponent %}
And this could be also used for merging of the HTML attributes inside the template:
@register.simple_tag
def merge_dicts(*dicts, **kwargs):
merged = {}
for d in dicts:
merged.update(d)
merged.update(kwargs)
return merged
{% merge_dicts
attrs
@click="(evt) => alert(evt.target.data)"
as tab_attrs %}
{% component "tab"
attrs=tab_attrs
%}{% endcomponent %}
❓Inlined custom tags
In the Custom tags examples, the tag still had to be defined on a separate line. I wonder if we could have a feature similar to React or Vue, where you could directly put logic as the value of the prop.
So what in React looks like:
<MyComp value={myVal ? thisIfTrue : thatIfFalse} />
Could possibly be achieved Django something like this:
{% component "my_comp"
value=`{% ternary tab.disabled this_if_true that_if_false %}`
%}{% endcomponent %}
Where the value wrapped in `{% ... %}`
would mean "interpret the content as template tag".
Implementation notes:
- Biggest question is whether the parser will handle the
`{% ... %}`
construct, because it may be threw off by the inner%}
. - To dynamically call a template tag, we'd use the fact that a template tag has access to the Parser instance (see example), and that it's the Parser which holds info on available template tags (see Parser source code).
- To parse the template tag inputs inside the
`{% ... %}`
, we'd use Django's smart_split, which is the same as what Django uses when it parses template tags. - We would omit
as var
from the inlined template tag, since it's implied that we return the output of it.
❓Spread operator
One last limitation when working with the components currently is that it doesn't have a spread operator. Again this is mostly useful when I want to combine my inputs with inputs given from outside.
For context, in React, it looks like this:
const person = { name: 'John', lastName: 'Smith'}
<Person age={29} {...person} />
// Which is same as
// <Person age={29} name={person.name} lastName={person.lastName} />
And Vue:
<Person :age="29" v-bind="person" />
// Same as
// <Person :age="29" :name="person.name" :last-name="person.lastName" />
In Django it could look like this:
{% component "person" age=29 ...person %}{% endcomponent %}
We already allow dots (.
) in kwarg names for components. So we would give special treatment to kwargs that start with ...
, and put the dict entries in it's place.