mirror of
https://github.com/django-components/django-components.git
synced 2025-07-08 01:45:00 +00:00
refactor: Add Node metadata (#1229)
* refactor: `Slot.source` replaced with `Slot.fill_node`, new `Component.node` property, and `slot_node` available in `on_slot_rendered()` hook. * refactor: fix windows path error in tests
This commit is contained in:
parent
abc6be343e
commit
46e524e37d
17 changed files with 728 additions and 103 deletions
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -158,12 +158,17 @@ Summary:
|
||||||
```py
|
```py
|
||||||
def render_to_response(
|
def render_to_response(
|
||||||
context: Optional[Union[Dict[str, Any], Context]] = None,
|
context: Optional[Union[Dict[str, Any], Context]] = None,
|
||||||
args: Optional[Tuple[Any, ...]] = None,
|
args: Optional[Any] = None,
|
||||||
kwargs: Optional[Mapping] = None,
|
kwargs: Optional[Any] = None,
|
||||||
slots: Optional[Mapping] = None,
|
slots: Optional[Any] = None,
|
||||||
deps_strategy: DependenciesStrategy = "document",
|
deps_strategy: DependenciesStrategy = "document",
|
||||||
render_dependencies: bool = True,
|
type: Optional[DependenciesStrategy] = None, # Deprecated, use `deps_strategy`
|
||||||
|
render_dependencies: bool = True, # Deprecated, use `deps_strategy="ignore"`
|
||||||
|
outer_context: Optional[Context] = None,
|
||||||
request: Optional[HttpRequest] = None,
|
request: Optional[HttpRequest] = None,
|
||||||
|
registry: Optional[ComponentRegistry] = None,
|
||||||
|
registered_name: Optional[str] = None,
|
||||||
|
node: Optional[ComponentNode] = None,
|
||||||
**response_kwargs: Any,
|
**response_kwargs: Any,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
```
|
```
|
||||||
|
@ -1008,6 +1013,13 @@ Summary:
|
||||||
|
|
||||||
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
||||||
|
|
||||||
|
- The `BaseNode` class also has two new metadata attributes:
|
||||||
|
|
||||||
|
- `template_name` - the name of the template that rendered the node.
|
||||||
|
- `template_component` - the component class that the template belongs to.
|
||||||
|
|
||||||
|
This is useful for debugging purposes.
|
||||||
|
|
||||||
- `Slot` class now has 3 new metadata fields:
|
- `Slot` class now has 3 new metadata fields:
|
||||||
|
|
||||||
1. `Slot.contents` attribute contains the original contents:
|
1. `Slot.contents` attribute contains the original contents:
|
||||||
|
@ -1018,10 +1030,11 @@ Summary:
|
||||||
|
|
||||||
2. `Slot.extra` attribute where you can put arbitrary metadata about the slot.
|
2. `Slot.extra` attribute where you can put arbitrary metadata about the slot.
|
||||||
|
|
||||||
3. `Slot.source` attribute tells where the slot comes from:
|
3. `Slot.fill_node` attribute tells where the slot comes from:
|
||||||
|
|
||||||
- `'template'` if the slot was created from `{% fill %}` tag.
|
- `FillNode` instance if the slot was created from `{% fill %}` tag.
|
||||||
- `'python'` if the slot was created from string, function, or `Slot` instance.
|
- `ComponentNode` instance if the slot was created as a default slot from a `{% component %}` tag.
|
||||||
|
- `None` if the slot was created from a string, function, or `Slot` instance.
|
||||||
|
|
||||||
See [Slot metadata](https://django-components.github.io/django-components/0.140/concepts/fundamentals/slots/#slot-metadata).
|
See [Slot metadata](https://django-components.github.io/django-components/0.140/concepts/fundamentals/slots/#slot-metadata).
|
||||||
|
|
||||||
|
@ -1048,6 +1061,46 @@ Summary:
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- You can now access the `{% component %}` tag (`ComponentNode` instance) from which a Component
|
||||||
|
was created. Use `Component.node` to access it.
|
||||||
|
|
||||||
|
This is mostly useful for extensions, which can use this to detect if the given Component
|
||||||
|
comes from a `{% component %}` tag or from a different source (such as `Component.render()`).
|
||||||
|
|
||||||
|
`Component.node` is `None` if the component is created by `Component.render()` (but you
|
||||||
|
can pass in the `node` kwarg yourself).
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, context, template):
|
||||||
|
if self.node is not None:
|
||||||
|
assert self.node.name == "my_component"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Node classes `ComponentNode`, `FillNode`, `ProvideNode`, and `SlotNode` are part of the public API.
|
||||||
|
|
||||||
|
These classes are what is instantiated when you use `{% component %}`, `{% fill %}`, `{% provide %}`, and `{% slot %}` tags.
|
||||||
|
|
||||||
|
You can for example use these for type hints:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component, ComponentNode
|
||||||
|
|
||||||
|
class MyTable(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
if kwargs.get("show_owner"):
|
||||||
|
node: Optional[ComponentNode] = self.node
|
||||||
|
owner: Optional[Component] = self.node.template_component
|
||||||
|
else:
|
||||||
|
node = None
|
||||||
|
owner = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"owner": owner,
|
||||||
|
"node": node,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
|
- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -153,12 +153,14 @@ GreetNode.register(library)
|
||||||
|
|
||||||
When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
|
When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
|
||||||
|
|
||||||
- `node_id`: A unique identifier for this node instance
|
- [`node_id`](../../../reference/api#django_components.BaseNode.node_id): A unique identifier for this node instance
|
||||||
- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
|
- [`flags`](../../../reference/api#django_components.BaseNode.flags): Dictionary of flag values (e.g. `{"required": True}`)
|
||||||
- `params`: List of raw parameters passed to the tag
|
- [`params`](../../../reference/api#django_components.BaseNode.params): List of raw parameters passed to the tag
|
||||||
- `nodelist`: The template nodes between the start and end tags
|
- [`nodelist`](../../../reference/api#django_components.BaseNode.nodelist): The template nodes between the start and end tags
|
||||||
- `contents`: The raw contents between the start and end tags
|
- [`contents`](../../../reference/api#django_components.BaseNode.contents): The raw contents between the start and end tags
|
||||||
- `active_flags`: List of flags that are currently set to True
|
- [`active_flags`](../../../reference/api#django_components.BaseNode.active_flags): List of flags that are currently set to True
|
||||||
|
- [`template_name`](../../../reference/api#django_components.BaseNode.template_name): The name of the `Template` instance inside which the node was defined
|
||||||
|
- [`template_component`](../../../reference/api#django_components.BaseNode.template_component): The component class that the `Template` belongs to
|
||||||
|
|
||||||
This is what the `node` parameter in the [`@template_tag`](../../../reference/api#django_components.template_tag) decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
This is what the `node` parameter in the [`@template_tag`](../../../reference/api#django_components.template_tag) decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ The Render API includes:
|
||||||
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
|
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
|
||||||
|
|
||||||
- Template tag metadata:
|
- Template tag metadata:
|
||||||
|
- [`self.node`](../render_api/#template-tag-metadata) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
|
||||||
- [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
- [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
||||||
- [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
|
- [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
|
||||||
- [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
- [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
||||||
|
@ -337,17 +338,18 @@ class Table(Component):
|
||||||
If the component is rendered with [`{% component %}`](../../../reference/template_tags#component) template tag,
|
If the component is rendered with [`{% component %}`](../../../reference/template_tags#component) template tag,
|
||||||
the following metadata is available:
|
the following metadata is available:
|
||||||
|
|
||||||
|
- [`self.node`](../../../reference/api/#django_components.Component.node) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
|
||||||
- [`self.registry`](../../../reference/api/#django_components.Component.registry) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
- [`self.registry`](../../../reference/api/#django_components.Component.registry) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
||||||
that was used to render the component
|
that was used to render the component
|
||||||
- [`self.registered_name`](../../../reference/api/#django_components.Component.registered_name) - The name under which the component was registered
|
- [`self.registered_name`](../../../reference/api/#django_components.Component.registered_name) - The name under which the component was registered
|
||||||
- [`self.outer_context`](../../../reference/api/#django_components.Component.outer_context) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
- [`self.outer_context`](../../../reference/api/#django_components.Component.outer_context) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% with abc=123 %}
|
{% with abc=123 %}
|
||||||
{{ abc }} {# <--- This is in outer context #}
|
{{ abc }} {# <--- This is in outer context #}
|
||||||
{% component "my_component" / %}
|
{% component "my_component" / %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use these to check whether the component was rendered inside a template with [`{% component %}`](../../../reference/template_tags#component) tag
|
You can use these to check whether the component was rendered inside a template with [`{% component %}`](../../../reference/template_tags#component) tag
|
||||||
or in Python with [`Component.render()`](../../../reference/api/#django_components.Component.render).
|
or in Python with [`Component.render()`](../../../reference/api/#django_components.Component.render).
|
||||||
|
@ -360,3 +362,40 @@ class MyComponent(Component):
|
||||||
else:
|
else:
|
||||||
# Do something for the {% component %} template tag
|
# Do something for the {% component %} template tag
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can access the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) under [`Component.node`](../../../reference/api/#django_components.Component.node):
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, context, template):
|
||||||
|
if self.node is not None:
|
||||||
|
assert self.node.name == "my_component"
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) is mostly useful for extensions, which can modify their behaviour based on the source of the Component.
|
||||||
|
|
||||||
|
For example, if `MyComponent` was used in another component - that is,
|
||||||
|
with a `{% component "my_component" %}` tag
|
||||||
|
in a template that belongs to another component - then you can use
|
||||||
|
[`self.node.template_component`](../../../reference/api/#django_components.ComponentNode.template_component)
|
||||||
|
to access the owner [`Component`](../../../reference/api/#django_components.Component) class.
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
class Parent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div>
|
||||||
|
{% component "my_component" / %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("my_component")
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, context, template):
|
||||||
|
if self.node is not None:
|
||||||
|
assert self.node.template_component == Parent
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
`Component.node` is `None` if the component is created by [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||||
|
(but you can pass in the `node` kwarg yourself).
|
||||||
|
|
|
@ -728,7 +728,7 @@ with extra metadata:
|
||||||
- [`component_name`](../../../reference/api#django_components.Slot.component_name)
|
- [`component_name`](../../../reference/api#django_components.Slot.component_name)
|
||||||
- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
|
- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
|
||||||
- [`nodelist`](../../../reference/api#django_components.Slot.nodelist)
|
- [`nodelist`](../../../reference/api#django_components.Slot.nodelist)
|
||||||
- [`source`](../../../reference/api#django_components.Slot.source)
|
- [`fill_node`](../../../reference/api#django_components.Slot.fill_node)
|
||||||
- [`extra`](../../../reference/api#django_components.Slot.extra)
|
- [`extra`](../../../reference/api#django_components.Slot.extra)
|
||||||
|
|
||||||
These are populated the first time a slot is passed to a component.
|
These are populated the first time a slot is passed to a component.
|
||||||
|
@ -738,10 +738,33 @@ still point to the first component that received the slot.
|
||||||
|
|
||||||
You can use these for debugging, such as printing out the slot's component name and slot name.
|
You can use these for debugging, such as printing out the slot's component name and slot name.
|
||||||
|
|
||||||
Extensions can use [`Slot.source`](../../../reference/api#django_components.Slot.source)
|
**Fill node**
|
||||||
|
|
||||||
|
Components or extensions can use [`Slot.fill_node`](../../../reference/api#django_components.Slot.fill_node)
|
||||||
to handle slots differently based on whether the slot
|
to handle slots differently based on whether the slot
|
||||||
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) tag
|
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) and
|
||||||
or in the component's Python code. See an example in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
|
[`{% component %}`](../../../reference/template_tags#component) tags,
|
||||||
|
or in the component's Python code.
|
||||||
|
|
||||||
|
If the slot was created from a [`{% fill %}`](../../../reference/template_tags#fill) tag,
|
||||||
|
this will be the [`FillNode`](../../../reference/api#django_components.FillNode) instance.
|
||||||
|
|
||||||
|
If the slot was a default slot created from a [`{% component %}`](../../../reference/template_tags#component) tag,
|
||||||
|
this will be the [`ComponentNode`](../../../reference/api#django_components.ComponentNode) instance.
|
||||||
|
|
||||||
|
You can use this to find the [`Component`](../../../reference/api#django_components.Component) in whose
|
||||||
|
template the [`{% fill %}`](../../../reference/template_tags#fill) tag was defined:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyTable(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
footer_slot = slots.get("footer")
|
||||||
|
if footer_slot is not None and footer_slot.fill_node is not None:
|
||||||
|
owner_component = footer_slot.fill_node.template_component
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Extra**
|
||||||
|
|
||||||
You can also pass any additional data along with the slot by setting it in [`Slot.extra`](../../../reference/api#django_components.Slot.extra):
|
You can also pass any additional data along with the slot by setting it in [`Slot.extra`](../../../reference/api#django_components.Slot.extra):
|
||||||
|
|
||||||
|
@ -761,7 +784,6 @@ slot = Slot(
|
||||||
# Optional
|
# Optional
|
||||||
component_name="table",
|
component_name="table",
|
||||||
slot_name="name",
|
slot_name="name",
|
||||||
source="python",
|
|
||||||
extra={},
|
extra={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -771,6 +793,8 @@ slot.slot_name = "name"
|
||||||
slot.extra["foo"] = "bar"
|
slot.extra["foo"] = "bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Read more in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
|
||||||
|
|
||||||
### Slot contents
|
### Slot contents
|
||||||
|
|
||||||
Whether you create a slot from a function, a string, or from the [`{% fill %}`](../../../reference/template_tags#fill) tags,
|
Whether you create a slot from a function, a string, or from the [`{% fill %}`](../../../reference/template_tags#fill) tags,
|
||||||
|
|
|
@ -47,6 +47,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ComponentNode
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentRegistry
|
::: django_components.ComponentRegistry
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -83,6 +87,14 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.FillNode
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ProvideNode
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.RegistrySettings
|
::: django_components.RegistrySettings
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -111,6 +123,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.SlotNode
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.SlotRef
|
::: django_components.SlotRef
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
|
@ -204,6 +204,7 @@ name | type | description
|
||||||
`slot_is_default` | `bool` | Whether the slot is default
|
`slot_is_default` | `bool` | Whether the slot is default
|
||||||
`slot_is_required` | `bool` | Whether the slot is required
|
`slot_is_required` | `bool` | Whether the slot is required
|
||||||
`slot_name` | `str` | The name of the `{% slot %}` tag
|
`slot_name` | `str` | The name of the `{% slot %}` tag
|
||||||
|
`slot_node` | `SlotNode` | The node instance of the `{% slot %}` tag
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3301" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3350" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ Renders one of the components that was previously registered with
|
||||||
[`@register()`](./api.md#django_components.register)
|
[`@register()`](./api.md#django_components.register)
|
||||||
decorator.
|
decorator.
|
||||||
|
|
||||||
The `{% component %}` tag takes:
|
The [`{% component %}`](../template_tags#component) tag takes:
|
||||||
|
|
||||||
- Component's registered name as the first positional argument,
|
- Component's registered name as the first positional argument,
|
||||||
- Followed by any number of positional and keyword arguments.
|
- Followed by any number of positional and keyword arguments.
|
||||||
|
@ -92,7 +92,8 @@ The component name must be a string literal.
|
||||||
### Inserting slot fills
|
### Inserting slot fills
|
||||||
|
|
||||||
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
|
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
|
||||||
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
|
"fill" these slots by placing the [`{% fill %}`](../template_tags#fill) tags
|
||||||
|
within the [`{% component %}`](../template_tags#component) tag:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "my_table" rows=rows headers=headers %}
|
{% component "my_table" rows=rows headers=headers %}
|
||||||
|
@ -102,7 +103,7 @@ If the component defined any [slots](../concepts/fundamentals/slots.md), you can
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can even nest [`{% fill %}`](#fill) tags within
|
You can even nest [`{% fill %}`](../template_tags#fill) tags within
|
||||||
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
|
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
|
||||||
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
|
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
|
||||||
and other tags:
|
and other tags:
|
||||||
|
@ -141,7 +142,7 @@ COMPONENTS = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Omitting the `component` keyword
|
### Omitting the component keyword
|
||||||
|
|
||||||
If you would like to omit the `component` keyword, and simply refer to your
|
If you would like to omit the `component` keyword, and simply refer to your
|
||||||
components by their registered names:
|
components by their registered names:
|
||||||
|
@ -169,19 +170,20 @@ COMPONENTS = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L948" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L988" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Use this tag to insert content into component's slots.
|
Use [`{% fill %}`](../template_tags#fill) tag to insert content into component's
|
||||||
|
[slots](../../concepts/fundamentals/slots).
|
||||||
|
|
||||||
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block,
|
[`{% fill %}`](../template_tags#fill) tag may be used only within a `{% component %}..{% endcomponent %}` block,
|
||||||
and raises a `TemplateSyntaxError` if used outside of a component.
|
and raises a `TemplateSyntaxError` if used outside of a component.
|
||||||
|
|
||||||
**Args:**
|
**Args:**
|
||||||
|
|
||||||
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
||||||
the default slot.
|
the [default slot](../../concepts/fundamentals/slots#default-slot).
|
||||||
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
||||||
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
|
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
- `fallback` (str, optional): This argument allows you to access the original content of the slot
|
- `fallback` (str, optional): This argument allows you to access the original content of the slot
|
||||||
|
@ -266,7 +268,7 @@ Fill:
|
||||||
### Using default slot
|
### Using default slot
|
||||||
|
|
||||||
To access slot data and the fallback slot content on the default slot,
|
To access slot data and the fallback slot content on the default slot,
|
||||||
use `{% fill %}` with `name` set to `"default"`:
|
use [`{% fill %}`](../template_tags#fill) with `name` set to `"default"`:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "button" %}
|
{% component "button" %}
|
||||||
|
@ -280,7 +282,7 @@ use `{% fill %}` with `name` set to `"default"`:
|
||||||
### Slot fills from Python
|
### Slot fills from Python
|
||||||
|
|
||||||
You can pass a slot fill from Python to a component by setting the `body` kwarg
|
You can pass a slot fill from Python to a component by setting the `body` kwarg
|
||||||
on the `{% fill %}` tag.
|
on the [`{% fill %}`](../template_tags#fill) tag.
|
||||||
|
|
||||||
First pass a [`Slot`](../api#django_components.Slot) instance to the template
|
First pass a [`Slot`](../api#django_components.Slot) instance to the template
|
||||||
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
|
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
|
||||||
|
@ -296,7 +298,7 @@ class Table(Component):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then pass the slot to the `{% fill %}` tag:
|
Then pass the slot to the [`{% fill %}`](../template_tags#fill) tag:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "table" %}
|
{% component "table" %}
|
||||||
|
@ -306,7 +308,7 @@ Then pass the slot to the `{% fill %}` tag:
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
If you define both the `body` kwarg and the `{% fill %}` tag's body,
|
If you define both the `body` kwarg and the [`{% fill %}`](../template_tags#fill) tag's body,
|
||||||
an error will be raised.
|
an error will be raised.
|
||||||
|
|
||||||
```django
|
```django
|
||||||
|
@ -391,8 +393,11 @@ See more usage examples in
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The "provider" part of the [provide / inject feature](../../concepts/advanced/provide_inject).
|
The [`{% provide %}`](../template_tags#provide) tag is part of the "provider" part of
|
||||||
|
the [provide / inject feature](../../concepts/advanced/provide_inject).
|
||||||
|
|
||||||
Pass kwargs to this tag to define the provider's data.
|
Pass kwargs to this tag to define the provider's data.
|
||||||
|
|
||||||
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
|
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
|
||||||
with [`Component.inject()`](../api#django_components.Component.inject).
|
with [`Component.inject()`](../api#django_components.Component.inject).
|
||||||
|
|
||||||
|
@ -445,7 +450,7 @@ class Child(Component):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice that the keys defined on the `{% provide %}` tag are then accessed as attributes
|
Notice that the keys defined on the [`{% provide %}`](../template_tags#provide) tag are then accessed as attributes
|
||||||
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
|
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
|
||||||
|
|
||||||
✅ Do this
|
✅ Do this
|
||||||
|
@ -467,11 +472,11 @@ user = self.inject("user_data")["user"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L474" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L513" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Slot tag marks a place inside a component where content can be inserted
|
[`{% slot %}`](../template_tags#slot) tag marks a place inside a component where content can be inserted
|
||||||
from outside.
|
from outside.
|
||||||
|
|
||||||
[Learn more](../../concepts/fundamentals/slots) about using slots.
|
[Learn more](../../concepts/fundamentals/slots) about using slots.
|
||||||
|
@ -485,7 +490,7 @@ or [React's `children`](https://react.dev/learn/passing-props-to-a-component#pas
|
||||||
|
|
||||||
- `name` (str, required): Registered name of the component to render
|
- `name` (str, required): Registered name of the component to render
|
||||||
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
|
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
|
||||||
without using the [`{% fill %}`](#fill) tag. See
|
without using the [`{% fill %}`](../template_tags#fill) tag. See
|
||||||
[Default slot](../../concepts/fundamentals/slots#default-slot)
|
[Default slot](../../concepts/fundamentals/slots#default-slot)
|
||||||
- `required`: Optional flag. Will raise an error if a slot is required but not given.
|
- `required`: Optional flag. Will raise an error if a slot is required but not given.
|
||||||
- `**kwargs`: Any extra kwargs will be passed as the slot data.
|
- `**kwargs`: Any extra kwargs will be passed as the slot data.
|
||||||
|
@ -527,8 +532,8 @@ class Parent(Component):
|
||||||
|
|
||||||
### Slot data
|
### Slot data
|
||||||
|
|
||||||
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
|
Any extra kwargs will be considered as slot data, and will be accessible
|
||||||
tag via fill's `data` kwarg:
|
in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
|
||||||
|
|
||||||
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
|
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
|
|
||||||
|
@ -565,8 +570,8 @@ class Parent(Component):
|
||||||
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
|
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
|
||||||
will be rendered if no fill is given for the slot.
|
will be rendered if no fill is given for the slot.
|
||||||
|
|
||||||
This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
|
This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
|
||||||
the fill's `fallback` kwarg.
|
using the fill's `fallback` kwarg.
|
||||||
This is useful if you need to wrap / prepend / append the original slot's content.
|
This is useful if you need to wrap / prepend / append the original slot's content.
|
||||||
|
|
||||||
```djc_py
|
```djc_py
|
||||||
|
|
|
@ -18,6 +18,7 @@ from django_components.util.command import (
|
||||||
from django_components.component import (
|
from django_components.component import (
|
||||||
Component,
|
Component,
|
||||||
ComponentInput,
|
ComponentInput,
|
||||||
|
ComponentNode,
|
||||||
ComponentVars,
|
ComponentVars,
|
||||||
all_components,
|
all_components,
|
||||||
get_component_by_class_id,
|
get_component_by_class_id,
|
||||||
|
@ -52,13 +53,16 @@ from django_components.extensions.debug_highlight import ComponentDebugHighlight
|
||||||
from django_components.extensions.view import ComponentView, get_component_url
|
from django_components.extensions.view import ComponentView, get_component_url
|
||||||
from django_components.library import TagProtectedError
|
from django_components.library import TagProtectedError
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
|
from django_components.provide import ProvideNode
|
||||||
from django_components.slots import (
|
from django_components.slots import (
|
||||||
|
FillNode,
|
||||||
Slot,
|
Slot,
|
||||||
SlotContent,
|
SlotContent,
|
||||||
SlotContext,
|
SlotContext,
|
||||||
SlotFallback,
|
SlotFallback,
|
||||||
SlotFunc,
|
SlotFunc,
|
||||||
SlotInput,
|
SlotInput,
|
||||||
|
SlotNode,
|
||||||
SlotRef,
|
SlotRef,
|
||||||
SlotResult,
|
SlotResult,
|
||||||
)
|
)
|
||||||
|
@ -103,6 +107,7 @@ __all__ = [
|
||||||
"ComponentInput",
|
"ComponentInput",
|
||||||
"ComponentMediaInput",
|
"ComponentMediaInput",
|
||||||
"ComponentMediaInputPath",
|
"ComponentMediaInputPath",
|
||||||
|
"ComponentNode",
|
||||||
"ComponentRegistry",
|
"ComponentRegistry",
|
||||||
"ComponentVars",
|
"ComponentVars",
|
||||||
"ComponentView",
|
"ComponentView",
|
||||||
|
@ -115,6 +120,7 @@ __all__ = [
|
||||||
"DynamicComponent",
|
"DynamicComponent",
|
||||||
"Empty",
|
"Empty",
|
||||||
"ExtensionComponentConfig",
|
"ExtensionComponentConfig",
|
||||||
|
"FillNode",
|
||||||
"format_attributes",
|
"format_attributes",
|
||||||
"get_component_by_class_id",
|
"get_component_by_class_id",
|
||||||
"get_component_dirs",
|
"get_component_dirs",
|
||||||
|
@ -131,6 +137,7 @@ __all__ = [
|
||||||
"OnComponentUnregisteredContext",
|
"OnComponentUnregisteredContext",
|
||||||
"OnRegistryCreatedContext",
|
"OnRegistryCreatedContext",
|
||||||
"OnRegistryDeletedContext",
|
"OnRegistryDeletedContext",
|
||||||
|
"ProvideNode",
|
||||||
"register",
|
"register",
|
||||||
"registry",
|
"registry",
|
||||||
"RegistrySettings",
|
"RegistrySettings",
|
||||||
|
@ -142,6 +149,7 @@ __all__ = [
|
||||||
"SlotFallback",
|
"SlotFallback",
|
||||||
"SlotFunc",
|
"SlotFunc",
|
||||||
"SlotInput",
|
"SlotInput",
|
||||||
|
"SlotNode",
|
||||||
"SlotRef",
|
"SlotRef",
|
||||||
"SlotResult",
|
"SlotResult",
|
||||||
"TagFormatterABC",
|
"TagFormatterABC",
|
||||||
|
|
|
@ -1941,6 +1941,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
slots: Optional[Any] = None,
|
slots: Optional[Any] = None,
|
||||||
deps_strategy: Optional[DependenciesStrategy] = None,
|
deps_strategy: Optional[DependenciesStrategy] = None,
|
||||||
request: Optional[HttpRequest] = None,
|
request: Optional[HttpRequest] = None,
|
||||||
|
node: Optional["ComponentNode"] = None,
|
||||||
id: Optional[str] = None,
|
id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
# TODO_v1 - Remove this whole block in v1. This is for backwards compatibility with pre-v0.140
|
# TODO_v1 - Remove this whole block in v1. This is for backwards compatibility with pre-v0.140
|
||||||
|
@ -2009,6 +2010,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.outer_context: Optional[Context] = outer_context
|
self.outer_context: Optional[Context] = outer_context
|
||||||
self.registry = default(registry, registry_)
|
self.registry = default(registry, registry_)
|
||||||
|
self.node = node
|
||||||
|
|
||||||
extensions._init_component_instance(self)
|
extensions._init_component_instance(self)
|
||||||
|
|
||||||
|
@ -2346,6 +2348,51 @@ class Component(metaclass=ComponentMeta):
|
||||||
that was used to render the component.
|
that was used to render the component.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
node: Optional["ComponentNode"]
|
||||||
|
"""
|
||||||
|
The [`ComponentNode`](../api/#django_components.ComponentNode) instance
|
||||||
|
that was used to render the component.
|
||||||
|
|
||||||
|
This will be set only if the component was rendered with the
|
||||||
|
[`{% component %}`](../template_tags#component) tag.
|
||||||
|
|
||||||
|
Accessing the [`ComponentNode`](../api/#django_components.ComponentNode) is mostly useful for extensions,
|
||||||
|
which can modify their behaviour based on the source of the Component.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, context, template):
|
||||||
|
if self.node is not None:
|
||||||
|
assert self.node.name == "my_component"
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if `MyComponent` was used in another component - that is,
|
||||||
|
with a `{% component "my_component" %}` tag
|
||||||
|
in a template that belongs to another component - then you can use
|
||||||
|
[`self.node.template_component`](../api/#django_components.ComponentNode.template_component)
|
||||||
|
to access the owner [`Component`](../api/#django_components.Component) class.
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
class Parent(Component):
|
||||||
|
template: types.django_html = '''
|
||||||
|
<div>
|
||||||
|
{% component "my_component" / %}
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
@register("my_component")
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, context, template):
|
||||||
|
if self.node is not None:
|
||||||
|
assert self.node.template_component == Parent
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
`Component.node` is `None` if the component is created by
|
||||||
|
[`Component.render()`](../api/#django_components.Component.render)
|
||||||
|
(but you can pass in the `node` kwarg yourself).
|
||||||
|
"""
|
||||||
# TODO_v1 - Remove, superseded by `Component.slots`
|
# TODO_v1 - Remove, superseded by `Component.slots`
|
||||||
is_filled: SlotIsFilled
|
is_filled: SlotIsFilled
|
||||||
"""
|
"""
|
||||||
|
@ -2535,6 +2582,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry: Optional[ComponentRegistry] = None,
|
registry: Optional[ComponentRegistry] = None,
|
||||||
registered_name: Optional[str] = None,
|
registered_name: Optional[str] = None,
|
||||||
|
node: Optional["ComponentNode"] = None,
|
||||||
**response_kwargs: Any,
|
**response_kwargs: Any,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
|
@ -2603,6 +2651,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry=registry,
|
registry=registry,
|
||||||
registered_name=registered_name,
|
registered_name=registered_name,
|
||||||
|
node=node,
|
||||||
)
|
)
|
||||||
return cls.response_class(content, **response_kwargs)
|
return cls.response_class(content, **response_kwargs)
|
||||||
|
|
||||||
|
@ -2623,6 +2672,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry: Optional[ComponentRegistry] = None,
|
registry: Optional[ComponentRegistry] = None,
|
||||||
registered_name: Optional[str] = None,
|
registered_name: Optional[str] = None,
|
||||||
|
node: Optional["ComponentNode"] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Render the component into a string. This is the equivalent of calling
|
Render the component into a string. This is the equivalent of calling
|
||||||
|
@ -2830,6 +2880,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry=registry,
|
registry=registry,
|
||||||
registered_name=registered_name,
|
registered_name=registered_name,
|
||||||
|
node=node,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is the internal entrypoint for the render function
|
# This is the internal entrypoint for the render function
|
||||||
|
@ -2846,6 +2897,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry: Optional[ComponentRegistry] = None,
|
registry: Optional[ComponentRegistry] = None,
|
||||||
registered_name: Optional[str] = None,
|
registered_name: Optional[str] = None,
|
||||||
|
node: Optional["ComponentNode"] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
component_name = _get_component_name(cls, registered_name)
|
component_name = _get_component_name(cls, registered_name)
|
||||||
|
|
||||||
|
@ -2862,6 +2914,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry=registry,
|
registry=registry,
|
||||||
registered_name=registered_name,
|
registered_name=registered_name,
|
||||||
|
node=node,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -2877,6 +2930,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry: Optional[ComponentRegistry] = None,
|
registry: Optional[ComponentRegistry] = None,
|
||||||
registered_name: Optional[str] = None,
|
registered_name: Optional[str] = None,
|
||||||
|
node: Optional["ComponentNode"] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
######################################
|
######################################
|
||||||
# 1. Handle inputs
|
# 1. Handle inputs
|
||||||
|
@ -2929,6 +2983,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
registry=registry,
|
registry=registry,
|
||||||
registered_name=registered_name,
|
registered_name=registered_name,
|
||||||
|
node=node,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Allow plugins to modify or validate the inputs
|
# Allow plugins to modify or validate the inputs
|
||||||
|
@ -3316,7 +3371,7 @@ class ComponentNode(BaseNode):
|
||||||
[`@register()`](./api.md#django_components.register)
|
[`@register()`](./api.md#django_components.register)
|
||||||
decorator.
|
decorator.
|
||||||
|
|
||||||
The `{% component %}` tag takes:
|
The [`{% component %}`](../template_tags#component) tag takes:
|
||||||
|
|
||||||
- Component's registered name as the first positional argument,
|
- Component's registered name as the first positional argument,
|
||||||
- Followed by any number of positional and keyword arguments.
|
- Followed by any number of positional and keyword arguments.
|
||||||
|
@ -3333,7 +3388,8 @@ class ComponentNode(BaseNode):
|
||||||
### Inserting slot fills
|
### Inserting slot fills
|
||||||
|
|
||||||
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
|
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
|
||||||
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
|
"fill" these slots by placing the [`{% fill %}`](../template_tags#fill) tags
|
||||||
|
within the [`{% component %}`](../template_tags#component) tag:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "my_table" rows=rows headers=headers %}
|
{% component "my_table" rows=rows headers=headers %}
|
||||||
|
@ -3343,7 +3399,7 @@ class ComponentNode(BaseNode):
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can even nest [`{% fill %}`](#fill) tags within
|
You can even nest [`{% fill %}`](../template_tags#fill) tags within
|
||||||
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
|
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
|
||||||
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
|
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
|
||||||
and other tags:
|
and other tags:
|
||||||
|
@ -3382,7 +3438,7 @@ class ComponentNode(BaseNode):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Omitting the `component` keyword
|
### Omitting the component keyword
|
||||||
|
|
||||||
If you would like to omit the `component` keyword, and simply refer to your
|
If you would like to omit the `component` keyword, and simply refer to your
|
||||||
components by their registered names:
|
components by their registered names:
|
||||||
|
@ -3417,6 +3473,8 @@ class ComponentNode(BaseNode):
|
||||||
nodelist: Optional[NodeList] = None,
|
nodelist: Optional[NodeList] = None,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
contents: Optional[str] = None,
|
contents: Optional[str] = None,
|
||||||
|
template_name: Optional[str] = None,
|
||||||
|
template_component: Optional[Type["Component"]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
params=params,
|
params=params,
|
||||||
|
@ -3424,6 +3482,8 @@ class ComponentNode(BaseNode):
|
||||||
nodelist=nodelist,
|
nodelist=nodelist,
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
|
template_name=template_name,
|
||||||
|
template_component=template_component,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -3499,6 +3559,7 @@ class ComponentNode(BaseNode):
|
||||||
registered_name=self.name,
|
registered_name=self.name,
|
||||||
outer_context=context,
|
outer_context=context,
|
||||||
registry=self.registry,
|
registry=self.registry,
|
||||||
|
node=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -28,7 +28,7 @@ from django_components.util.routing import URLRoute
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.slots import Slot, SlotResult
|
from django_components.slots import Slot, SlotNode, SlotResult
|
||||||
|
|
||||||
|
|
||||||
TCallable = TypeVar("TCallable", bound=Callable)
|
TCallable = TypeVar("TCallable", bound=Callable)
|
||||||
|
@ -155,6 +155,8 @@ class OnSlotRenderedContext(NamedTuple):
|
||||||
"""The Slot instance that was rendered"""
|
"""The Slot instance that was rendered"""
|
||||||
slot_name: str
|
slot_name: str
|
||||||
"""The name of the `{% slot %}` tag"""
|
"""The name of the `{% slot %}` tag"""
|
||||||
|
slot_node: "SlotNode"
|
||||||
|
"""The node instance of the `{% slot %}` tag"""
|
||||||
slot_is_required: bool
|
slot_is_required: bool
|
||||||
"""Whether the slot is required"""
|
"""Whether the slot is required"""
|
||||||
slot_is_default: bool
|
slot_is_default: bool
|
||||||
|
@ -744,6 +746,26 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
||||||
# Append a comment to the slot's rendered output
|
# Append a comment to the slot's rendered output
|
||||||
return ctx.result + "<!-- MyExtension comment -->"
|
return ctx.result + "<!-- MyExtension comment -->"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Access slot metadata:**
|
||||||
|
|
||||||
|
You can access the [`{% slot %}` tag](../template_tags#slot)
|
||||||
|
node ([`SlotNode`](../api#django_components.SlotNode)) and its metadata using `ctx.slot_node`.
|
||||||
|
|
||||||
|
For example, to find the [`Component`](../api#django_components.Component) class to which
|
||||||
|
belongs the template where the [`{% slot %}`](../template_tags#slot) tag is defined, you can use
|
||||||
|
[`ctx.slot_node.template_component`](../api#django_components.SlotNode.template_component):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import ComponentExtension, OnSlotRenderedContext
|
||||||
|
|
||||||
|
class MyExtension(ComponentExtension):
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
# Access slot metadata
|
||||||
|
slot_node = ctx.slot_node
|
||||||
|
slot_owner = slot_node.template_component
|
||||||
|
print(f"Slot owner: {slot_owner}")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import keyword
|
import keyword
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, cast
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, cast
|
||||||
|
|
||||||
from django.template import Context, Library
|
from django.template import Context, Library
|
||||||
from django.template.base import Node, NodeList, Parser, Token
|
from django.template.base import Node, NodeList, Parser, Token
|
||||||
|
@ -15,6 +15,9 @@ from django_components.util.template_tag import (
|
||||||
validate_params,
|
validate_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.component import Component
|
||||||
|
|
||||||
|
|
||||||
# Normally, when `Node.render()` is called, it receives only a single argument `context`.
|
# Normally, when `Node.render()` is called, it receives only a single argument `context`.
|
||||||
#
|
#
|
||||||
|
@ -252,32 +255,77 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
# PUBLIC API (Configurable by users)
|
# PUBLIC API (Configurable by users)
|
||||||
# #####################################
|
# #####################################
|
||||||
|
|
||||||
tag: str
|
tag: ClassVar[str]
|
||||||
"""
|
"""
|
||||||
The tag name.
|
The tag name.
|
||||||
|
|
||||||
E.g. `"component"` or `"slot"` will make this class match
|
E.g. `"component"` or `"slot"` will make this class match
|
||||||
template tags `{% component %}` or `{% slot %}`.
|
template tags `{% component %}` or `{% slot %}`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SlotNode(BaseNode):
|
||||||
|
tag = "slot"
|
||||||
|
end_tag = "endslot"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow the template tag `{% slot %}` to be used like this:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot %} ... {% endslot %}
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
end_tag: Optional[str] = None
|
end_tag: ClassVar[Optional[str]] = None
|
||||||
"""
|
"""
|
||||||
The end tag name.
|
The end tag name.
|
||||||
|
|
||||||
E.g. `"endcomponent"` or `"endslot"` will make this class match
|
E.g. `"endcomponent"` or `"endslot"` will make this class match
|
||||||
template tags `{% endcomponent %}` or `{% endslot %}`.
|
template tags `{% endcomponent %}` or `{% endslot %}`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SlotNode(BaseNode):
|
||||||
|
tag = "slot"
|
||||||
|
end_tag = "endslot"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow the template tag `{% slot %}` to be used like this:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot %} ... {% endslot %}
|
||||||
|
```
|
||||||
|
|
||||||
If not set, then this template tag has no end tag.
|
If not set, then this template tag has no end tag.
|
||||||
|
|
||||||
So instead of `{% component %} ... {% endcomponent %}`, you'd use only
|
So instead of `{% component %} ... {% endcomponent %}`, you'd use only
|
||||||
`{% component %}`.
|
`{% component %}`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyNode(BaseNode):
|
||||||
|
tag = "mytag"
|
||||||
|
end_tag = None
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allowed_flags: Optional[List[str]] = None
|
allowed_flags: ClassVar[Optional[List[str]]] = None
|
||||||
"""
|
"""
|
||||||
The allowed flags for this tag.
|
The list of all *possible* flags for this tag.
|
||||||
|
|
||||||
E.g. `["required"]` will allow this tag to be used like `{% slot required %}`.
|
E.g. `["required"]` will allow this tag to be used like `{% slot required %}`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SlotNode(BaseNode):
|
||||||
|
tag = "slot"
|
||||||
|
end_tag = "endslot"
|
||||||
|
allowed_flags = ["required", "default"]
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow the template tag `{% slot %}` to be used like this:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot required %} ... {% endslot %}
|
||||||
|
{% slot default %} ... {% endslot %}
|
||||||
|
{% slot required default %} ... {% endslot %}
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def render(self, context: Context, *args: Any, **kwargs: Any) -> str:
|
def render(self, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||||
|
@ -303,6 +351,133 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
"""
|
"""
|
||||||
return self.nodelist.render(context)
|
return self.nodelist.render(context)
|
||||||
|
|
||||||
|
# #####################################
|
||||||
|
# Attributes
|
||||||
|
# #####################################
|
||||||
|
|
||||||
|
params: List[TagAttr]
|
||||||
|
"""
|
||||||
|
The parameters to the tag in the template.
|
||||||
|
|
||||||
|
A single param represents an arg or kwarg of the template tag.
|
||||||
|
|
||||||
|
E.g. the following tag:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% component "my_comp" key=val key2='val2 two' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Has 3 params:
|
||||||
|
|
||||||
|
- Posiitonal arg `"my_comp"`
|
||||||
|
- Keyword arg `key=val`
|
||||||
|
- Keyword arg `key2='val2 two'`
|
||||||
|
"""
|
||||||
|
|
||||||
|
flags: Dict[str, bool]
|
||||||
|
"""
|
||||||
|
Dictionary of all [`allowed_flags`](../api#django_components.BaseNode.allowed_flags)
|
||||||
|
that were set on the tag.
|
||||||
|
|
||||||
|
Flags that were set are `True`, and the rest are `False`.
|
||||||
|
|
||||||
|
E.g. the following tag:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SlotNode(BaseNode):
|
||||||
|
tag = "slot"
|
||||||
|
end_tag = "endslot"
|
||||||
|
allowed_flags = ["default", "required"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot "content" default %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Has 2 flags, `default` and `required`, but only `default` was set.
|
||||||
|
|
||||||
|
The `flags` dictionary will be:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"default": True,
|
||||||
|
"required": False,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check if a flag is set by doing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if node.flags["default"]:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodelist: NodeList
|
||||||
|
"""
|
||||||
|
The nodelist of the tag.
|
||||||
|
|
||||||
|
This is the text between the opening and closing tags, e.g.
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot "content" default required %}
|
||||||
|
<div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `nodelist` will contain the `<div> ... </div>` part.
|
||||||
|
|
||||||
|
Unlike [`contents`](../api#django_components.BaseNode.contents),
|
||||||
|
the `nodelist` contains the actual Nodes, not just the text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
contents: Optional[str]
|
||||||
|
"""
|
||||||
|
The contents of the tag.
|
||||||
|
|
||||||
|
This is the text between the opening and closing tags, e.g.
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot "content" default required %}
|
||||||
|
<div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
{% endslot %}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `contents` will be `"<div> ... </div>"`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_id: str
|
||||||
|
"""
|
||||||
|
The unique ID of the node.
|
||||||
|
|
||||||
|
Extensions can use this ID to store additional information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name: Optional[str]
|
||||||
|
"""
|
||||||
|
The name of the [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||||
|
that contains this node.
|
||||||
|
|
||||||
|
The template name is set by Django's
|
||||||
|
[template loaders](https://docs.djangoproject.com/en/5.2/topics/templates/#loaders).
|
||||||
|
|
||||||
|
For example, the filesystem template loader will set this to the absolute path of the template file.
|
||||||
|
|
||||||
|
```
|
||||||
|
"/home/user/project/templates/my_template.html"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_component: Optional[Type["Component"]]
|
||||||
|
"""
|
||||||
|
If the template that contains this node belongs to a [`Component`](../api#django_components.Component),
|
||||||
|
then this will be the [`Component`](../api#django_components.Component) class.
|
||||||
|
"""
|
||||||
|
|
||||||
# #####################################
|
# #####################################
|
||||||
# MISC
|
# MISC
|
||||||
# #####################################
|
# #####################################
|
||||||
|
@ -314,12 +489,16 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
nodelist: Optional[NodeList] = None,
|
nodelist: Optional[NodeList] = None,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
contents: Optional[str] = None,
|
contents: Optional[str] = None,
|
||||||
|
template_name: Optional[str] = None,
|
||||||
|
template_component: Optional[Type["Component"]] = None,
|
||||||
):
|
):
|
||||||
self.params = params
|
self.params = params
|
||||||
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
|
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
|
||||||
self.nodelist = nodelist or NodeList()
|
self.nodelist = nodelist or NodeList()
|
||||||
self.node_id = node_id or gen_id()
|
self.node_id = node_id or gen_id()
|
||||||
self.contents = contents
|
self.contents = contents
|
||||||
|
self.template_name = template_name
|
||||||
|
self.template_component = template_component
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
|
@ -329,7 +508,21 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_flags(self) -> List[str]:
|
def active_flags(self) -> List[str]:
|
||||||
"""Flags that were set for this specific instance."""
|
"""
|
||||||
|
Flags that were set for this specific instance as a list of strings.
|
||||||
|
|
||||||
|
E.g. the following tag:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% slot "content" default required / %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Will have the following flags:
|
||||||
|
|
||||||
|
```python
|
||||||
|
["default", "required"]
|
||||||
|
```
|
||||||
|
"""
|
||||||
flags = []
|
flags = []
|
||||||
for flag, value in self.flags.items():
|
for flag, value in self.flags.items():
|
||||||
if value:
|
if value:
|
||||||
|
@ -347,6 +540,9 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
|
|
||||||
To register the tag, you can use [`BaseNode.register()`](../api#django_components.BaseNode.register).
|
To register the tag, you can use [`BaseNode.register()`](../api#django_components.BaseNode.register).
|
||||||
"""
|
"""
|
||||||
|
# NOTE: Avoids circular import
|
||||||
|
from django_components.template import get_component_from_origin
|
||||||
|
|
||||||
tag_id = gen_id()
|
tag_id = gen_id()
|
||||||
tag = parse_template_tag(cls.tag, cls.end_tag, cls.allowed_flags, parser, token)
|
tag = parse_template_tag(cls.tag, cls.end_tag, cls.allowed_flags, parser, token)
|
||||||
|
|
||||||
|
@ -359,6 +555,8 @@ class BaseNode(Node, metaclass=NodeMeta):
|
||||||
params=tag.params,
|
params=tag.params,
|
||||||
flags=tag.flags,
|
flags=tag.flags,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
|
template_name=parser.origin.name if parser.origin else None,
|
||||||
|
template_component=get_component_from_origin(parser.origin) if parser.origin else None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,11 @@ from django_components.util.misc import gen_id
|
||||||
|
|
||||||
class ProvideNode(BaseNode):
|
class ProvideNode(BaseNode):
|
||||||
"""
|
"""
|
||||||
The "provider" part of the [provide / inject feature](../../concepts/advanced/provide_inject).
|
The [`{% provide %}`](../template_tags#provide) tag is part of the "provider" part of
|
||||||
|
the [provide / inject feature](../../concepts/advanced/provide_inject).
|
||||||
|
|
||||||
Pass kwargs to this tag to define the provider's data.
|
Pass kwargs to this tag to define the provider's data.
|
||||||
|
|
||||||
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
|
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
|
||||||
with [`Component.inject()`](../api#django_components.Component.inject).
|
with [`Component.inject()`](../api#django_components.Component.inject).
|
||||||
|
|
||||||
|
@ -66,7 +69,7 @@ class ProvideNode(BaseNode):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice that the keys defined on the `{% provide %}` tag are then accessed as attributes
|
Notice that the keys defined on the [`{% provide %}`](../template_tags#provide) tag are then accessed as attributes
|
||||||
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
|
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
|
||||||
|
|
||||||
✅ Do this
|
✅ Do this
|
||||||
|
|
|
@ -265,14 +265,33 @@ class Slot(Generic[TSlotData]):
|
||||||
|
|
||||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||||
"""
|
"""
|
||||||
source: Literal["template", "python"] = "python"
|
fill_node: Optional[Union["FillNode", "ComponentNode"]] = None
|
||||||
"""
|
"""
|
||||||
Whether the slot was created from a [`{% fill %}`](../template_tags#fill) tag (`'template'`),
|
If the slot was created from a [`{% fill %}`](../template_tags#fill) tag,
|
||||||
or Python (`'python'`).
|
this will be the [`FillNode`](../api/#django_components.FillNode) instance.
|
||||||
|
|
||||||
|
If the slot was a default slot created from a [`{% component %}`](../template_tags#component) tag,
|
||||||
|
this will be the [`ComponentNode`](../api/#django_components.ComponentNode) instance.
|
||||||
|
|
||||||
|
Otherwise, this will be `None`.
|
||||||
|
|
||||||
Extensions can use this info to handle slots differently based on their source.
|
Extensions can use this info to handle slots differently based on their source.
|
||||||
|
|
||||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
You can use this to find the [`Component`](../api/#django_components.Component) in whose
|
||||||
|
template the [`{% fill %}`](../template_tags#fill) tag was defined:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyTable(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
footer_slot = slots.get("footer")
|
||||||
|
if footer_slot is not None and footer_slot.fill_node is not None:
|
||||||
|
owner_component = footer_slot.fill_node.template_component
|
||||||
|
# ...
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
extra: Dict[str, Any] = field(default_factory=dict)
|
extra: Dict[str, Any] = field(default_factory=dict)
|
||||||
"""
|
"""
|
||||||
|
@ -494,7 +513,7 @@ class SlotIsFilled(dict):
|
||||||
|
|
||||||
class SlotNode(BaseNode):
|
class SlotNode(BaseNode):
|
||||||
"""
|
"""
|
||||||
Slot tag marks a place inside a component where content can be inserted
|
[`{% slot %}`](../template_tags#slot) tag marks a place inside a component where content can be inserted
|
||||||
from outside.
|
from outside.
|
||||||
|
|
||||||
[Learn more](../../concepts/fundamentals/slots) about using slots.
|
[Learn more](../../concepts/fundamentals/slots) about using slots.
|
||||||
|
@ -508,7 +527,7 @@ class SlotNode(BaseNode):
|
||||||
|
|
||||||
- `name` (str, required): Registered name of the component to render
|
- `name` (str, required): Registered name of the component to render
|
||||||
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
|
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
|
||||||
without using the [`{% fill %}`](#fill) tag. See
|
without using the [`{% fill %}`](../template_tags#fill) tag. See
|
||||||
[Default slot](../../concepts/fundamentals/slots#default-slot)
|
[Default slot](../../concepts/fundamentals/slots#default-slot)
|
||||||
- `required`: Optional flag. Will raise an error if a slot is required but not given.
|
- `required`: Optional flag. Will raise an error if a slot is required but not given.
|
||||||
- `**kwargs`: Any extra kwargs will be passed as the slot data.
|
- `**kwargs`: Any extra kwargs will be passed as the slot data.
|
||||||
|
@ -550,8 +569,8 @@ class SlotNode(BaseNode):
|
||||||
|
|
||||||
### Slot data
|
### Slot data
|
||||||
|
|
||||||
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
|
Any extra kwargs will be considered as slot data, and will be accessible
|
||||||
tag via fill's `data` kwarg:
|
in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
|
||||||
|
|
||||||
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
|
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
|
|
||||||
|
@ -588,8 +607,8 @@ class SlotNode(BaseNode):
|
||||||
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
|
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
|
||||||
will be rendered if no fill is given for the slot.
|
will be rendered if no fill is given for the slot.
|
||||||
|
|
||||||
This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
|
This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
|
||||||
the fill's `fallback` kwarg.
|
using the fill's `fallback` kwarg.
|
||||||
This is useful if you need to wrap / prepend / append the original slot's content.
|
This is useful if you need to wrap / prepend / append the original slot's content.
|
||||||
|
|
||||||
```djc_py
|
```djc_py
|
||||||
|
@ -926,6 +945,7 @@ class SlotNode(BaseNode):
|
||||||
component_id=component_id,
|
component_id=component_id,
|
||||||
slot=slot,
|
slot=slot,
|
||||||
slot_name=slot_name,
|
slot_name=slot_name,
|
||||||
|
slot_node=self,
|
||||||
slot_is_required=is_required,
|
slot_is_required=is_required,
|
||||||
slot_is_default=is_default,
|
slot_is_default=is_default,
|
||||||
result=output,
|
result=output,
|
||||||
|
@ -968,15 +988,16 @@ class SlotNode(BaseNode):
|
||||||
|
|
||||||
class FillNode(BaseNode):
|
class FillNode(BaseNode):
|
||||||
"""
|
"""
|
||||||
Use this tag to insert content into component's slots.
|
Use [`{% fill %}`](../template_tags#fill) tag to insert content into component's
|
||||||
|
[slots](../../concepts/fundamentals/slots).
|
||||||
|
|
||||||
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block,
|
[`{% fill %}`](../template_tags#fill) tag may be used only within a `{% component %}..{% endcomponent %}` block,
|
||||||
and raises a `TemplateSyntaxError` if used outside of a component.
|
and raises a `TemplateSyntaxError` if used outside of a component.
|
||||||
|
|
||||||
**Args:**
|
**Args:**
|
||||||
|
|
||||||
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
||||||
the default slot.
|
the [default slot](../../concepts/fundamentals/slots#default-slot).
|
||||||
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
||||||
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
|
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
- `fallback` (str, optional): This argument allows you to access the original content of the slot
|
- `fallback` (str, optional): This argument allows you to access the original content of the slot
|
||||||
|
@ -1061,7 +1082,7 @@ class FillNode(BaseNode):
|
||||||
### Using default slot
|
### Using default slot
|
||||||
|
|
||||||
To access slot data and the fallback slot content on the default slot,
|
To access slot data and the fallback slot content on the default slot,
|
||||||
use `{% fill %}` with `name` set to `"default"`:
|
use [`{% fill %}`](../template_tags#fill) with `name` set to `"default"`:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "button" %}
|
{% component "button" %}
|
||||||
|
@ -1075,7 +1096,7 @@ class FillNode(BaseNode):
|
||||||
### Slot fills from Python
|
### Slot fills from Python
|
||||||
|
|
||||||
You can pass a slot fill from Python to a component by setting the `body` kwarg
|
You can pass a slot fill from Python to a component by setting the `body` kwarg
|
||||||
on the `{% fill %}` tag.
|
on the [`{% fill %}`](../template_tags#fill) tag.
|
||||||
|
|
||||||
First pass a [`Slot`](../api#django_components.Slot) instance to the template
|
First pass a [`Slot`](../api#django_components.Slot) instance to the template
|
||||||
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
|
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
|
||||||
|
@ -1091,7 +1112,7 @@ class FillNode(BaseNode):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then pass the slot to the `{% fill %}` tag:
|
Then pass the slot to the [`{% fill %}`](../template_tags#fill) tag:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "table" %}
|
{% component "table" %}
|
||||||
|
@ -1101,7 +1122,7 @@ class FillNode(BaseNode):
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
If you define both the `body` kwarg and the `{% fill %}` tag's body,
|
If you define both the `body` kwarg and the [`{% fill %}`](../template_tags#fill) tag's body,
|
||||||
an error will be raised.
|
an error will be raised.
|
||||||
|
|
||||||
```django
|
```django
|
||||||
|
@ -1395,7 +1416,7 @@ def resolve_fills(
|
||||||
contents=contents,
|
contents=contents,
|
||||||
data_var=None,
|
data_var=None,
|
||||||
fallback_var=None,
|
fallback_var=None,
|
||||||
source="template",
|
fill_node=component_node,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The content has fills
|
# The content has fills
|
||||||
|
@ -1405,14 +1426,15 @@ def resolve_fills(
|
||||||
for fill in maybe_fills:
|
for fill in maybe_fills:
|
||||||
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
|
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
|
||||||
if fill.body is not None:
|
if fill.body is not None:
|
||||||
|
# Set `Slot.fill_node` so the slot fill behaves the same as if it was defined inside
|
||||||
|
# a `{% fill %}` tag.
|
||||||
|
# This for example allows CSS scoping to work even on slots that are defined
|
||||||
|
# as `{% fill ... body=... / %}`
|
||||||
if isinstance(fill.body, Slot):
|
if isinstance(fill.body, Slot):
|
||||||
# Make a copy of the Slot instance and set it to `source="template"`,
|
# Make a copy of the Slot instance and set its `fill_node`.
|
||||||
# so it behaves the same as if the content was written inside the `{% fill %}` tag.
|
slot_fill = dataclass_replace(fill.body, fill_node=fill.fill)
|
||||||
# This for example allows CSS scoping to work even on slots that are defined
|
|
||||||
# as `{% fill ... body=... / %}`
|
|
||||||
slot_fill = dataclass_replace(fill.body, source="template")
|
|
||||||
else:
|
else:
|
||||||
slot_fill = Slot(fill.body)
|
slot_fill = Slot(fill.body, fill_node=fill.fill)
|
||||||
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
||||||
else:
|
else:
|
||||||
slot_fill = _nodelist_to_slot(
|
slot_fill = _nodelist_to_slot(
|
||||||
|
@ -1423,7 +1445,7 @@ def resolve_fills(
|
||||||
data_var=fill.data_var,
|
data_var=fill.data_var,
|
||||||
fallback_var=fill.fallback_var,
|
fallback_var=fill.fallback_var,
|
||||||
extra_context=fill.extra_context,
|
extra_context=fill.extra_context,
|
||||||
source="template",
|
fill_node=fill.fill,
|
||||||
)
|
)
|
||||||
slots[fill.name] = slot_fill
|
slots[fill.name] = slot_fill
|
||||||
|
|
||||||
|
@ -1497,14 +1519,14 @@ def normalize_slot_fills(
|
||||||
used_slot_name = content.slot_name or slot_name
|
used_slot_name = content.slot_name or slot_name
|
||||||
used_nodelist = content.nodelist
|
used_nodelist = content.nodelist
|
||||||
used_contents = content.contents if content.contents is not None else content_func
|
used_contents = content.contents if content.contents is not None else content_func
|
||||||
used_source = content.source
|
used_fill_node = content.fill_node
|
||||||
used_extra = content.extra.copy()
|
used_extra = content.extra.copy()
|
||||||
else:
|
else:
|
||||||
used_component_name = component_name
|
used_component_name = component_name
|
||||||
used_slot_name = slot_name
|
used_slot_name = slot_name
|
||||||
used_nodelist = None
|
used_nodelist = None
|
||||||
used_contents = content_func
|
used_contents = content_func
|
||||||
used_source = "python"
|
used_fill_node = None
|
||||||
used_extra = {}
|
used_extra = {}
|
||||||
|
|
||||||
slot = Slot(
|
slot = Slot(
|
||||||
|
@ -1513,7 +1535,7 @@ def normalize_slot_fills(
|
||||||
component_name=used_component_name,
|
component_name=used_component_name,
|
||||||
slot_name=used_slot_name,
|
slot_name=used_slot_name,
|
||||||
nodelist=used_nodelist,
|
nodelist=used_nodelist,
|
||||||
source=used_source,
|
fill_node=used_fill_node,
|
||||||
extra=used_extra,
|
extra=used_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1544,7 +1566,7 @@ def _nodelist_to_slot(
|
||||||
data_var: Optional[str] = None,
|
data_var: Optional[str] = None,
|
||||||
fallback_var: Optional[str] = None,
|
fallback_var: Optional[str] = None,
|
||||||
extra_context: Optional[Dict[str, Any]] = None,
|
extra_context: Optional[Dict[str, Any]] = None,
|
||||||
source: Optional[Literal["template", "python"]] = None,
|
fill_node: Optional[Union[FillNode, "ComponentNode"]] = None,
|
||||||
extra: Optional[Dict[str, Any]] = None,
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
) -> Slot:
|
) -> Slot:
|
||||||
if data_var:
|
if data_var:
|
||||||
|
@ -1638,7 +1660,7 @@ def _nodelist_to_slot(
|
||||||
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
|
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
|
||||||
# So we need to special-case this.
|
# So we need to special-case this.
|
||||||
contents=default(contents, ""),
|
contents=default(contents, ""),
|
||||||
source=default(source, "python"),
|
fill_node=default(fill_node, None),
|
||||||
extra=default(extra, {}),
|
extra=default(extra, {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ Tests focusing on the Component class.
|
||||||
For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||||
|
|
||||||
from django_components import (
|
from django_components import (
|
||||||
Component,
|
Component,
|
||||||
|
ComponentRegistry,
|
||||||
ComponentView,
|
ComponentView,
|
||||||
Slot,
|
Slot,
|
||||||
SlotInput,
|
SlotInput,
|
||||||
|
@ -598,6 +600,110 @@ class TestComponentRenderAPI:
|
||||||
assert comp.slots == {} # type: ignore[attr-defined]
|
assert comp.slots == {} # type: ignore[attr-defined]
|
||||||
assert comp.context == Context() # type: ignore[attr-defined]
|
assert comp.context == Context() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
def test_metadata__template(self):
|
||||||
|
comp: Any = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "hello"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal comp
|
||||||
|
comp = self
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div class="test-component">
|
||||||
|
{% component "test" / %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context())
|
||||||
|
|
||||||
|
assertHTMLEqual(rendered, '<div class="test-component">hello</div>')
|
||||||
|
|
||||||
|
assert isinstance(comp, TestComponent)
|
||||||
|
|
||||||
|
assert isinstance(comp.outer_context, Context)
|
||||||
|
assert comp.outer_context == Context()
|
||||||
|
|
||||||
|
assert isinstance(comp.registry, ComponentRegistry)
|
||||||
|
assert comp.registered_name == "test"
|
||||||
|
|
||||||
|
assert comp.node is not None
|
||||||
|
assert comp.node.template_component is None
|
||||||
|
assert comp.node.template_name == "<unknown source>"
|
||||||
|
|
||||||
|
def test_metadata__component(self):
|
||||||
|
comp: Any = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "hello"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal comp
|
||||||
|
comp = self
|
||||||
|
|
||||||
|
class Outer(Component):
|
||||||
|
template = "{% component 'test' only / %}"
|
||||||
|
|
||||||
|
rendered = Outer.render()
|
||||||
|
|
||||||
|
assert rendered == 'hello'
|
||||||
|
|
||||||
|
assert isinstance(comp, TestComponent)
|
||||||
|
|
||||||
|
assert isinstance(comp.outer_context, Context)
|
||||||
|
assert comp.outer_context is not comp.context
|
||||||
|
|
||||||
|
assert isinstance(comp.registry, ComponentRegistry)
|
||||||
|
assert comp.registered_name == "test"
|
||||||
|
|
||||||
|
assert comp.node is not None
|
||||||
|
assert comp.node.template_component == Outer
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
assert comp.node.template_name.endswith("tests\\test_component.py::Outer") # type: ignore
|
||||||
|
else:
|
||||||
|
assert comp.node.template_name.endswith("tests/test_component.py::Outer") # type: ignore
|
||||||
|
|
||||||
|
def test_metadata__python(self):
|
||||||
|
comp: Any = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "hello"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal comp
|
||||||
|
comp = self
|
||||||
|
|
||||||
|
rendered = TestComponent.render(
|
||||||
|
context=Context(),
|
||||||
|
args=(),
|
||||||
|
kwargs={},
|
||||||
|
slots={},
|
||||||
|
deps_strategy="document",
|
||||||
|
render_dependencies=True,
|
||||||
|
request=None,
|
||||||
|
outer_context=Context(),
|
||||||
|
registry=ComponentRegistry(),
|
||||||
|
registered_name="test",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered == 'hello'
|
||||||
|
|
||||||
|
assert isinstance(comp, TestComponent)
|
||||||
|
|
||||||
|
assert isinstance(comp.outer_context, Context)
|
||||||
|
assert comp.outer_context == Context()
|
||||||
|
|
||||||
|
assert isinstance(comp.registry, ComponentRegistry)
|
||||||
|
assert comp.registered_name == "test"
|
||||||
|
|
||||||
|
assert comp.node is None
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentTemplateVars:
|
class TestComponentTemplateVars:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.http import HttpRequest, HttpResponse
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
||||||
from django_components import Component, Slot, register, registry
|
from django_components import Component, Slot, SlotNode, register, registry
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.extension import (
|
from django_components.extension import (
|
||||||
|
@ -140,6 +140,13 @@ class RenderExtension(ComponentExtension):
|
||||||
name = "render"
|
name = "render"
|
||||||
|
|
||||||
|
|
||||||
|
class SlotOverrideExtension(ComponentExtension):
|
||||||
|
name = "slot_override"
|
||||||
|
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
||||||
|
return "OVERRIDEN BY EXTENSION"
|
||||||
|
|
||||||
|
|
||||||
def with_component_cls(on_created: Callable):
|
def with_component_cls(on_created: Callable):
|
||||||
class TempComponent(Component):
|
class TempComponent(Component):
|
||||||
template = "Hello {{ name }}!"
|
template = "Hello {{ name }}!"
|
||||||
|
@ -341,13 +348,15 @@ class TestExtensionHooks:
|
||||||
|
|
||||||
# Render the component with some args and kwargs
|
# Render the component with some args and kwargs
|
||||||
test_context = Context({"foo": "bar"})
|
test_context = Context({"foo": "bar"})
|
||||||
TestComponent.render(
|
rendered = TestComponent.render(
|
||||||
context=test_context,
|
context=test_context,
|
||||||
args=("arg1", "arg2"),
|
args=("arg1", "arg2"),
|
||||||
kwargs={"name": "Test"},
|
kwargs={"name": "Test"},
|
||||||
slots={"content": "Some content"},
|
slots={"content": "Some content"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert rendered == "Hello Some content!"
|
||||||
|
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||||
|
|
||||||
# Verify on_slot_rendered was called with correct args
|
# Verify on_slot_rendered was called with correct args
|
||||||
|
@ -359,10 +368,25 @@ class TestExtensionHooks:
|
||||||
assert slot_call.component_id == "ca1bc3e"
|
assert slot_call.component_id == "ca1bc3e"
|
||||||
assert isinstance(slot_call.slot, Slot)
|
assert isinstance(slot_call.slot, Slot)
|
||||||
assert slot_call.slot_name == "content"
|
assert slot_call.slot_name == "content"
|
||||||
|
assert isinstance(slot_call.slot_node, SlotNode)
|
||||||
|
assert slot_call.slot_node.template_name.endswith("test_extension.py::TestComponent") # type: ignore
|
||||||
|
assert slot_call.slot_node.template_component == TestComponent
|
||||||
assert slot_call.slot_is_required is True
|
assert slot_call.slot_is_required is True
|
||||||
assert slot_call.slot_is_default is True
|
assert slot_call.slot_is_default is True
|
||||||
assert slot_call.result == "Some content"
|
assert slot_call.result == "Some content"
|
||||||
|
|
||||||
|
@djc_test(components_settings={"extensions": [SlotOverrideExtension]})
|
||||||
|
def test_on_slot_rendered__override(self):
|
||||||
|
@register("test_comp")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hello {% slot 'content' required default / %}!"
|
||||||
|
|
||||||
|
rendered = TestComponent.render(
|
||||||
|
slots={"content": "Some content"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rendered == "Hello OVERRIDEN BY EXTENSION!"
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestExtensionViews:
|
class TestExtensionViews:
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.template.base import TextNode, VariableNode
|
from django.template.base import TextNode, VariableNode
|
||||||
from django.template.defaulttags import IfNode, LoremNode
|
from django.template.defaulttags import IfNode, LoremNode
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
|
|
||||||
from django_components import types
|
from django_components import Component, types
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
from django_components.templatetags import component_tags
|
from django_components.templatetags import component_tags
|
||||||
from django_components.util.tag_parser import TagAttr
|
from django_components.util.tag_parser import TagAttr
|
||||||
|
@ -849,7 +852,14 @@ class TestSignatureBasedValidation:
|
||||||
@force_signature_validation
|
@force_signature_validation
|
||||||
def render(self, context: Context, name: str, **kwargs) -> str:
|
def render(self, context: Context, name: str, **kwargs) -> str:
|
||||||
nonlocal captured
|
nonlocal captured
|
||||||
captured = self.params, self.nodelist, self.node_id, self.contents
|
captured = (
|
||||||
|
self.params,
|
||||||
|
self.nodelist,
|
||||||
|
self.node_id,
|
||||||
|
self.contents,
|
||||||
|
self.template_name,
|
||||||
|
self.template_component,
|
||||||
|
)
|
||||||
return f"Hello, {name}!"
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
# Case 1 - Node with end tag and NOT self-closing
|
# Case 1 - Node with end tag and NOT self-closing
|
||||||
|
@ -864,7 +874,7 @@ class TestSignatureBasedValidation:
|
||||||
template1 = Template(template_str1)
|
template1 = Template(template_str1)
|
||||||
template1.render(Context({}))
|
template1.render(Context({}))
|
||||||
|
|
||||||
params1, nodelist1, node_id1, contents1 = captured # type: ignore
|
params1, nodelist1, node_id1, contents1, template_name1, template_component1 = captured # type: ignore
|
||||||
assert len(params1) == 1
|
assert len(params1) == 1
|
||||||
assert isinstance(params1[0], TagAttr)
|
assert isinstance(params1[0], TagAttr)
|
||||||
# NOTE: The comment node is not included in the nodelist
|
# NOTE: The comment node is not included in the nodelist
|
||||||
|
@ -879,6 +889,8 @@ class TestSignatureBasedValidation:
|
||||||
assert isinstance(nodelist1[7], TextNode)
|
assert isinstance(nodelist1[7], TextNode)
|
||||||
assert contents1 == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501
|
assert contents1 == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501
|
||||||
assert node_id1 == "a1bc3e"
|
assert node_id1 == "a1bc3e"
|
||||||
|
assert template_name1 == '<unknown source>'
|
||||||
|
assert template_component1 is None
|
||||||
|
|
||||||
captured = None # Reset captured
|
captured = None # Reset captured
|
||||||
|
|
||||||
|
@ -890,12 +902,14 @@ class TestSignatureBasedValidation:
|
||||||
template2 = Template(template_str2)
|
template2 = Template(template_str2)
|
||||||
template2.render(Context({}))
|
template2.render(Context({}))
|
||||||
|
|
||||||
params2, nodelist2, node_id2, contents2 = captured # type: ignore
|
params2, nodelist2, node_id2, contents2, template_name2, template_component2 = captured # type: ignore
|
||||||
assert len(params2) == 1 # type: ignore
|
assert len(params2) == 1 # type: ignore
|
||||||
assert isinstance(params2[0], TagAttr) # type: ignore
|
assert isinstance(params2[0], TagAttr) # type: ignore
|
||||||
assert len(nodelist2) == 0 # type: ignore
|
assert len(nodelist2) == 0 # type: ignore
|
||||||
assert contents2 is None # type: ignore
|
assert contents2 is None # type: ignore
|
||||||
assert node_id2 == "a1bc3f" # type: ignore
|
assert node_id2 == "a1bc3f" # type: ignore
|
||||||
|
assert template_name2 == '<unknown source>' # type: ignore
|
||||||
|
assert template_component2 is None # type: ignore
|
||||||
|
|
||||||
captured = None # Reset captured
|
captured = None # Reset captured
|
||||||
|
|
||||||
|
@ -906,7 +920,7 @@ class TestSignatureBasedValidation:
|
||||||
@force_signature_validation
|
@force_signature_validation
|
||||||
def render(self, context: Context, name: str, **kwargs) -> str:
|
def render(self, context: Context, name: str, **kwargs) -> str:
|
||||||
nonlocal captured
|
nonlocal captured
|
||||||
captured = self.params, self.nodelist, self.node_id, self.contents
|
captured = self.params, self.nodelist, self.node_id, self.contents, self.template_name, self.template_component # noqa: E501
|
||||||
return f"Hello, {name}!"
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
TestNodeWithoutEndTag.register(component_tags.register)
|
TestNodeWithoutEndTag.register(component_tags.register)
|
||||||
|
@ -918,12 +932,38 @@ class TestSignatureBasedValidation:
|
||||||
template3 = Template(template_str3)
|
template3 = Template(template_str3)
|
||||||
template3.render(Context({}))
|
template3.render(Context({}))
|
||||||
|
|
||||||
params3, nodelist3, node_id3, contents3 = captured # type: ignore
|
params3, nodelist3, node_id3, contents3, template_name3, template_component3 = captured # type: ignore
|
||||||
assert len(params3) == 1 # type: ignore
|
assert len(params3) == 1 # type: ignore
|
||||||
assert isinstance(params3[0], TagAttr) # type: ignore
|
assert isinstance(params3[0], TagAttr) # type: ignore
|
||||||
assert len(nodelist3) == 0 # type: ignore
|
assert len(nodelist3) == 0 # type: ignore
|
||||||
assert contents3 is None # type: ignore
|
assert contents3 is None # type: ignore
|
||||||
assert node_id3 == "a1bc40" # type: ignore
|
assert node_id3 == "a1bc40" # type: ignore
|
||||||
|
assert template_name3 == '<unknown source>' # type: ignore
|
||||||
|
assert template_component3 is None # type: ignore
|
||||||
|
|
||||||
|
# Case 4 - Node nested in Component end tag
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% mytag2 'John' %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
TestComponent.render(Context({}))
|
||||||
|
|
||||||
|
params4, nodelist4, node_id4, contents4, template_name4, template_component4 = captured # type: ignore
|
||||||
|
assert len(params4) == 1 # type: ignore
|
||||||
|
assert isinstance(params4[0], TagAttr) # type: ignore
|
||||||
|
assert len(nodelist4) == 0 # type: ignore
|
||||||
|
assert contents4 is None # type: ignore
|
||||||
|
assert node_id4 == "a1bc42" # type: ignore
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
assert cast(str, template_name4).endswith("\\tests\\test_node.py::TestComponent") # type: ignore
|
||||||
|
else:
|
||||||
|
assert cast(str, template_name4).endswith("/tests/test_node.py::TestComponent") # type: ignore
|
||||||
|
|
||||||
|
assert template_name4 == f"{__file__}::TestComponent" # type: ignore
|
||||||
|
assert template_component4 is TestComponent # type: ignore
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
TestNodeWithEndTag.unregister(component_tags.register)
|
TestNodeWithEndTag.unregister(component_tags.register)
|
||||||
|
|
|
@ -12,7 +12,8 @@ from django.template.base import NodeList, TextNode
|
||||||
from pytest_django.asserts import assertHTMLEqual
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
from django_components.slots import Slot, SlotContext, SlotFallback
|
from django_components.component import ComponentNode
|
||||||
|
from django_components.slots import FillNode, Slot, SlotContext, SlotFallback
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
@ -431,7 +432,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "SimpleComponent"
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
assert first_slot_func.slot_name == "first"
|
assert first_slot_func.slot_name == "first"
|
||||||
assert first_slot_func.source == "python"
|
assert first_slot_func.fill_node is None
|
||||||
assert first_slot_func.extra == {}
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
@ -465,7 +466,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "SimpleComponent"
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
assert first_slot_func.slot_name == "first"
|
assert first_slot_func.slot_name == "first"
|
||||||
assert first_slot_func.source == "python"
|
assert first_slot_func.fill_node is None
|
||||||
assert first_slot_func.extra == {}
|
assert first_slot_func.extra == {}
|
||||||
assert first_slot_func.nodelist is None
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
|
@ -495,7 +496,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "SimpleComponent"
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
assert first_slot_func.slot_name == "whoop"
|
assert first_slot_func.slot_name == "whoop"
|
||||||
assert first_slot_func.source == "python"
|
assert first_slot_func.fill_node is None
|
||||||
assert first_slot_func.extra == {"foo": "bar"}
|
assert first_slot_func.extra == {"foo": "bar"}
|
||||||
assert first_slot_func.nodelist is None
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
|
@ -530,7 +531,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "test"
|
assert first_slot_func.component_name == "test"
|
||||||
assert first_slot_func.slot_name == "default"
|
assert first_slot_func.slot_name == "default"
|
||||||
assert first_slot_func.source == "template"
|
assert isinstance(first_slot_func.fill_node, ComponentNode)
|
||||||
assert first_slot_func.extra == {}
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
@ -571,7 +572,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "test"
|
assert first_slot_func.component_name == "test"
|
||||||
assert first_slot_func.slot_name == "first"
|
assert first_slot_func.slot_name == "first"
|
||||||
assert first_slot_func.source == "template"
|
assert isinstance(first_slot_func.fill_node, FillNode)
|
||||||
assert first_slot_func.extra == {}
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
@ -616,7 +617,7 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.component_name == "test"
|
assert first_slot_func.component_name == "test"
|
||||||
assert first_slot_func.slot_name == "whoop"
|
assert first_slot_func.slot_name == "whoop"
|
||||||
assert first_slot_func.source == "template"
|
assert isinstance(first_slot_func.fill_node, FillNode)
|
||||||
assert first_slot_func.extra == {"foo": "bar"}
|
assert first_slot_func.extra == {"foo": "bar"}
|
||||||
assert first_slot_func.nodelist is None
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue