mirror of
https://github.com/django-components/django-components.git
synced 2025-08-10 01:08:00 +00:00
feat: @template_tag and refactor how template tags are defined (#910)
This commit is contained in:
parent
a047908189
commit
f908197850
23 changed files with 2149 additions and 1148 deletions
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Authoring component libraries
|
||||
weight: 8
|
||||
weight: 9
|
||||
---
|
||||
|
||||
You can publish and share your components for others to use. Below you will find the steps to do so.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Tag formatters
|
||||
weight: 7
|
||||
weight: 8
|
||||
---
|
||||
|
||||
## Customizing component tags with TagFormatter
|
||||
|
|
197
docs/concepts/advanced/template_tags.md
Normal file
197
docs/concepts/advanced/template_tags.md
Normal file
|
@ -0,0 +1,197 @@
|
|||
---
|
||||
title: Custom template tags
|
||||
weight: 7
|
||||
---
|
||||
|
||||
Template tags introduced by django-components, such as `{% component %}` and `{% slot %}`,
|
||||
offer additional features over the default Django template tags:
|
||||
|
||||
<!-- # TODO - Update docs regarding literal lists and dictionaries
|
||||
- Using literal lists and dictionaries
|
||||
- Comments inside and tag with `{# ... #}`
|
||||
-->
|
||||
- [Self-closing tags `{% mytag / %}`](../../fundamentals/template_tag_syntax#self-closing-tags)
|
||||
- [Allowing the use of `:`, `-` (and more) in keys](../../fundamentals/template_tag_syntax#special-characters)
|
||||
- [Spread operator `...`](../../fundamentals/template_tag_syntax#spread-operator)
|
||||
- [Using template tags as inputs to other template tags](../../fundamentals/template_tag_syntax#use-template-tags-inside-component-inputs)
|
||||
- [Flat definition of dictionaries `attr:key=val`](../../fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs)
|
||||
- Function-like input validation
|
||||
|
||||
You too can easily create custom template tags that use the above features.
|
||||
|
||||
## Defining template tags with `@template_tag`
|
||||
|
||||
The simplest way to create a custom template tag is using
|
||||
the [`template_tag`](../../../reference/api#django_components.template_tag) decorator.
|
||||
This decorator allows you to define a template tag by just writing a function that returns the rendered content.
|
||||
|
||||
```python
|
||||
from django.template import Context, Library
|
||||
from django_components import BaseNode, template_tag
|
||||
|
||||
library = Library()
|
||||
|
||||
@template_tag(
|
||||
library,
|
||||
tag="mytag",
|
||||
end_tag="endmytag",
|
||||
allowed_flags=["required"]
|
||||
)
|
||||
def mytag(node: BaseNode, context: Context, name: str, **kwargs) -> str:
|
||||
return f"Hello, {name}!"
|
||||
```
|
||||
|
||||
This will allow you to use the tag in your templates like this:
|
||||
|
||||
```django
|
||||
{% mytag name="John" %}
|
||||
{% endmytag %}
|
||||
|
||||
{# or with self-closing syntax #}
|
||||
{% mytag name="John" / %}
|
||||
|
||||
{# or with flags #}
|
||||
{% mytag name="John" required %}
|
||||
{% endmytag %}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
The `@template_tag` decorator accepts the following parameters:
|
||||
|
||||
- `library`: The Django template library to register the tag with
|
||||
- `tag`: The name of the template tag (e.g. `"mytag"` for `{% mytag %}`)
|
||||
- `end_tag`: Optional. The name of the end tag (e.g. `"endmytag"` for `{% endmytag %}`)
|
||||
- `allowed_flags`: Optional. List of flags that can be used with the tag (e.g. `["required"]` for `{% mytag required %}`)
|
||||
|
||||
### Function signature
|
||||
|
||||
The function decorated with `@template_tag` must accept at least two arguments:
|
||||
|
||||
1. `node`: The node instance (we'll explain this in detail in the next section)
|
||||
2. `context`: The Django template context
|
||||
|
||||
Any additional parameters in your function's signature define what inputs your template tag accepts. For example:
|
||||
|
||||
```python
|
||||
@template_tag(library, tag="greet")
|
||||
def greet(
|
||||
node: BaseNode,
|
||||
context: Context,
|
||||
name: str, # required positional argument
|
||||
count: int = 1, # optional positional argument
|
||||
*, # keyword-only arguments marker
|
||||
msg: str, # required keyword argument
|
||||
mode: str = "default", # optional keyword argument
|
||||
) -> str:
|
||||
return f"{msg}, {name}!" * count
|
||||
```
|
||||
|
||||
This allows the tag to be used like:
|
||||
|
||||
```django
|
||||
{# All parameters #}
|
||||
{% greet "John" count=2 msg="Hello" mode="custom" %}
|
||||
|
||||
{# Only required parameters #}
|
||||
{% greet "John" msg="Hello" %}
|
||||
|
||||
{# Missing required parameter - will raise error #}
|
||||
{% greet "John" %} {# Error: missing 'msg' #}
|
||||
```
|
||||
|
||||
When you pass input to a template tag, it behaves the same way as if you passed the input to a function:
|
||||
|
||||
- If required parameters are missing, an error is raised
|
||||
- If unexpected parameters are passed, an error is raised
|
||||
|
||||
To accept keys that are not valid Python identifiers (e.g. `data-id`), or would conflict with Python keywords (e.g. `is`), you can use the `**kwargs` syntax:
|
||||
|
||||
```python
|
||||
@template_tag(library, tag="greet")
|
||||
def greet(
|
||||
node: BaseNode,
|
||||
context: Context,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
attrs = kwargs.copy()
|
||||
is_var = attrs.pop("is", None)
|
||||
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
||||
|
||||
return mark_safe(f"""
|
||||
<div {attrs_str}>
|
||||
Hello, {is_var}!
|
||||
</div>
|
||||
""")
|
||||
```
|
||||
|
||||
This allows you to use the tag like this:
|
||||
|
||||
```django
|
||||
{% greet is="John" data-id="123" %}
|
||||
```
|
||||
|
||||
## Defining template tags with `BaseNode`
|
||||
|
||||
For more control over your template tag, you can subclass [`BaseNode`](../../../reference/api#django_components.BaseNode) directly instead of using the decorator. This gives you access to additional features like the node's internal state and parsing details.
|
||||
|
||||
```python
|
||||
from django_components import BaseNode
|
||||
|
||||
class GreetNode(BaseNode):
|
||||
tag = "greet"
|
||||
end_tag = "endgreet"
|
||||
allowed_flags = ["required"]
|
||||
|
||||
def render(self, context: Context, name: str, **kwargs) -> str:
|
||||
# Access node properties
|
||||
if self.flags["required"]:
|
||||
return f"Required greeting: Hello, {name}!"
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# Register the node
|
||||
GreetNode.register(library)
|
||||
```
|
||||
|
||||
### Node properties
|
||||
|
||||
When using `BaseNode`, you have access to several useful properties:
|
||||
|
||||
- `node_id`: A unique identifier for this node instance
|
||||
- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
|
||||
- `params`: List of raw parameters passed to the tag
|
||||
- `nodelist`: The template nodes between the start and end tags
|
||||
- `active_flags`: List of flags that are currently set to True
|
||||
|
||||
This is what the `node` parameter in the `@template_tag` decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||
|
||||
### Rendering content between tags
|
||||
|
||||
When your tag has an end tag, you can access and render the content between the tags using `nodelist`:
|
||||
|
||||
```python
|
||||
class WrapNode(BaseNode):
|
||||
tag = "wrap"
|
||||
end_tag = "endwrap"
|
||||
|
||||
def render(self, context: Context, tag: str = "div", **attrs) -> str:
|
||||
# Render the content between tags
|
||||
inner = self.nodelist.render(context)
|
||||
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
|
||||
return f"<{tag} {attrs_str}>{inner}</{tag}>"
|
||||
|
||||
# Usage:
|
||||
{% wrap tag="section" class="content" %}
|
||||
Hello, world!
|
||||
{% endwrap %}
|
||||
```
|
||||
|
||||
### Unregistering nodes
|
||||
|
||||
You can unregister a node from a library using the `unregister` method:
|
||||
|
||||
```python
|
||||
GreetNode.unregister(library)
|
||||
```
|
||||
|
||||
This is particularly useful in testing when you want to clean up after registering temporary tags.
|
|
@ -49,8 +49,8 @@ from django.urls import URLPattern, URLResolver
|
|||
|
||||
from django_components import ComponentVars, TagFormatterABC
|
||||
from django_components.component import Component
|
||||
from django_components.node import BaseNode
|
||||
from django_components.util.misc import get_import_path
|
||||
from django_components.util.template_tag import TagSpec
|
||||
|
||||
# NOTE: This file is an entrypoint for the `gen-files` plugin in `mkdocs.yml`.
|
||||
# However, `gen-files` plugin runs this file as a script, NOT as a module.
|
||||
|
@ -504,17 +504,18 @@ def gen_reference_templatetags():
|
|||
f"Import as\n```django\n{{% load {mod_name} %}}\n```\n\n"
|
||||
)
|
||||
|
||||
for name, obj in inspect.getmembers(tags_module):
|
||||
for _, obj in inspect.getmembers(tags_module):
|
||||
if not _is_template_tag(obj):
|
||||
continue
|
||||
|
||||
tag_spec: TagSpec = obj._tag_spec
|
||||
tag_signature = _format_tag_signature(tag_spec)
|
||||
obj_lineno = inspect.findsource(obj)[1]
|
||||
node_cls: BaseNode = obj
|
||||
name = node_cls.tag
|
||||
tag_signature = _format_tag_signature(node_cls)
|
||||
obj_lineno = inspect.findsource(node_cls)[1]
|
||||
source_code_link = _format_source_code_html(module_rel_path, obj_lineno)
|
||||
|
||||
# Use the tag's function's docstring
|
||||
docstring = dedent(obj.__doc__ or "").strip()
|
||||
docstring = dedent(node_cls.__doc__ or "").strip()
|
||||
|
||||
# Rebuild (almost) the same documentation than as if we used
|
||||
# mkdocstrings' `::: path.to.module` syntax.
|
||||
|
@ -585,29 +586,29 @@ def _list_urls(urlpatterns: Sequence[Union[URLPattern, URLResolver]], prefix="")
|
|||
return urls
|
||||
|
||||
|
||||
def _format_tag_signature(tag_spec: TagSpec) -> str:
|
||||
def _format_tag_signature(node_cls: BaseNode) -> str:
|
||||
"""
|
||||
Given the TagSpec instance, format the tag's function signature like:
|
||||
Given the Node class, format the tag's function signature like:
|
||||
```django
|
||||
{% component [arg, ...] **kwargs [only] %}
|
||||
{% component arg1: int, arg2: str, *args, **kwargs: Any [only] %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
"""
|
||||
# The signature returns a string like:
|
||||
# `(arg: Any, **kwargs: Any) -> None`
|
||||
params_str = str(tag_spec.signature)
|
||||
params_str = str(node_cls._signature)
|
||||
# Remove the return type annotation, the `-> None` part
|
||||
params_str = params_str.rsplit("->", 1)[0]
|
||||
# Remove brackets around the params, to end up only with `arg: Any, **kwargs: Any`
|
||||
params_str = params_str.strip()[1:-1]
|
||||
|
||||
if tag_spec.flags:
|
||||
params_str += " " + " ".join([f"[{name}]" for name in tag_spec.flags])
|
||||
if node_cls.allowed_flags:
|
||||
params_str += " " + " ".join([f"[{name}]" for name in node_cls.allowed_flags])
|
||||
|
||||
# Create the function signature
|
||||
full_tag = "{% " + tag_spec.tag + " " + params_str + " %}"
|
||||
if tag_spec.end_tag:
|
||||
full_tag += f"\n{{% {tag_spec.end_tag} %}}"
|
||||
full_tag = "{% " + node_cls.tag + " " + params_str + " %}"
|
||||
if node_cls.end_tag:
|
||||
full_tag += f"\n{{% {node_cls.end_tag} %}}"
|
||||
|
||||
return full_tag
|
||||
|
||||
|
@ -722,7 +723,7 @@ def _is_tag_formatter_instance(obj: Any) -> bool:
|
|||
|
||||
|
||||
def _is_template_tag(obj: Any) -> bool:
|
||||
return callable(obj) and hasattr(obj, "_tag_spec")
|
||||
return inspect.isclass(obj) and issubclass(obj, BaseNode)
|
||||
|
||||
|
||||
def gen_reference():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue