Open JuroOravec opened 2 weeks ago
I think this is a really nice addition to the library!
Awesome!
Turns out that there is more work required to get to this feature:
ComponentRegistry
too, so it works with the assumption that each component may have a different template tag.django_components/__init__.py
the API's entrypoint. So users will import from django_components
, instead of django_components.component
, as it is currently. And it allows us to internally import from django_components.component
.To slowly move towards https://github.com/EmilStenstrom/django-components/issues/473, I am also documenting the registry and the autoimport files/features. So before we get to the "tag formatter" feature, there will be at least these 3 PRs:
django_components.component
to django_components
@JuroOravec Sounds awesome. Very happy to review many smaller PRs instead of 1000 line ones :)
Another week, another feature! @EmilStenstrom Let me know what you think about this one.
Description
When I was considering libraries like django_components, I felt that the way how components are declared in the template is too lengthy:
and the appeal of django-slippers or django-web-components was that they had a terser syntax, e.g.:
This has, I think, two important considerations:
In django-web-components, they did a really interesting solution to this, which is that they allow users to decide what tags they want to use for the components. See Component tag formatter.
In their case, one can configure the used template tags as so:
Which then allows them to call components inside the template as so:
API
TagFormatter class
I gave this a try. For django_components, this had to be a bit modified, because we use the
component
tag, which is shared by all components. So in our case it's not sufficient to know only about the tag, but we need to also get a second piece of information (the component name) to know how to render it. Conversely, if we used tags like#my_comp
, then we'd have the component name already in the tag.For this reason, I had to add also
parse_block_start_tag
andparse_inline_tag
, where the user would have the chance to parse tag inputs, and extract info that specifies what component it is.See TagFormatter API
```py class TagFormatter(): def format_block_start_tag(self, name: str) -> str: """Formats the start tag of a block component.""" ... def format_block_end_tag(self, name: str) -> str: """Formats the end tag of a block component.""" ... def format_inline_tag(self, name: str) -> str: """Formats the start tag of an inline component.""" ... def parse_block_start_tag(self, tokens: List[str]) -> Tuple[str, List[str]]: """ Given the tokens (words) of a component start tag, this function extracts the component name from the tokens list, and returns a tuple of `(component_name, component_input)`. Example: Given a component declarations: `{% component "my_comp" key=val key2=val2 %}` This function receives a list of tokens `['component', '"my_comp"', 'key=val', 'key2=val2']` `component` is the tag name, which we drop. `"my_comp"` is the component name, but we must remove the extra quotes. And we pass remaining tokens unmodified, as that's the input to the component. So in the end, we return a tuple: `('my_comp', ['key=val', 'key2=val2'])` """ ... def parse_inline_tag(self, tokens: List[str]) -> Tuple[str, List[str]]: """Same as `parse_block_start_tag`, but for inline components.""" ... ```Here's an example how we'd use the TagFormatter to use the components with the
{% component %}
(as it is now):See ComponentTagFormatter
```py class ComponentTagFormatter(TagFormatterABC): def format_block_start_tag(self, name: str) -> str: return "component" def format_block_end_tag(self, name: str) -> str: return "endcomponent" def format_inline_tag(self, name: str) -> str: return "#component" def parse_block_start_tag(self, tokens: List[str]) -> Tuple[str, List[str]]: if tokens[0] != "component": raise TemplateSyntaxError(f"Component block start tag parser received tag '{tokens[0]}', expected 'component'") return self._parse_start_or_inline_tag(tokens) def parse_inline_tag(self, tokens: List[str]) -> Tuple[str, List[str]]: if tokens[0] != "#component": raise TemplateSyntaxError(f"Component block start tag parser received tag '{tokens[0]}', expected '#component'") return self._parse_start_or_inline_tag(tokens) # _parse_start_or_inline_tag() omitted for clarityy ```tag_formatter
settingAs shown before, in django-web-components, they set this TagFormatter as app settings. I think it makes sense to define it there, so that ALL components are declared the same way throughout the app.
They define the tag formatter as an import path. Again, I think this makes sense, as this approach is used a lot in Django's settings.
So in our case it'd look like this:
However, to make things easier for when working in tests, and to get errors when the import path changes, I allowed to also pass the TagFormatter class directly:
Pre-defined TagFormatters for the original and "short" forms
To make it easy for users, I've created two TagFormatter.
ComponentTagFormatter
as seen above. This is the default tag formatter to keep the original behavior intactShorthandTagFormatter
- Tag formatter that behaves like in django-web-components, so instead of{% component "my_comp" %}
, you use{% my_comp %}
, and instead of{% endcomponent %}
, you use{% endmy_comp %}
."Inline" components
This means that this feature would again introduce the "inline" component tags, meaning
{% component %}
tags without{% endcomponent %}
, and so with no slots fill. E.g.:Instead of