8000 Proposals to make it possible to keep HTML attributes inside the HTML file · Issue #539 · django-components/django-components · GitHub
[go: up one dir, main page]

Skip to content
Proposals to make it possible to keep HTML attributes inside the HTML file #539
Closed
< 8000 div class="Box-sc-g0xbh4-0 gRssIw">@JuroOravec

Description

@JuroOravec

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 component tab. I want to add some extra HTML attrs to it (@click=...). Ideally the @click=... HTML attribute could live in the template, directly on the tab component. But because I needed to merge my HTML attribute with the user-given attrs, 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 accepts editable, which in this case is the negation of disabled. I cannot do editable=not disabled inside the template, so I had to convert the list of TabItems to a list of dictionaries, so I could define an extra key readonly.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone
      < 2DE3 /div>

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0