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:
Juro Oravec 2025-06-03 12:58:48 +02:00 committed by GitHub
parent abc6be343e
commit 46e524e37d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 728 additions and 103 deletions

View file

@ -158,12 +158,17 @@ Summary:
```py
def render_to_response(
context: Optional[Union[Dict[str, Any], Context]] = None,
args: Optional[Tuple[Any, ...]] = None,
kwargs: Optional[Mapping] = None,
slots: Optional[Mapping] = None,
args: Optional[Any] = None,
kwargs: Optional[Any] = None,
slots: Optional[Any] = None,
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,
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional[ComponentNode] = None,
**response_kwargs: Any,
) -> HttpResponse:
```
@ -1008,6 +1013,13 @@ Summary:
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:
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.
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.
- `'python'` if the slot was created from string, function, or `Slot` instance.
- `FillNode` instance if the slot was created from `{% fill %}` tag.
- `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).
@ -1048,6 +1061,46 @@ Summary:
{% 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`.
```py

View file

@ -153,12 +153,14 @@ GreetNode.register(library)
When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
- `node_id`: A unique identifier for this node instance
- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
- `params`: List of raw parameters passed to the tag
- `nodelist`: The template nodes between the start and end tags
- `contents`: The raw contents between the start and end tags
- `active_flags`: List of flags that are currently set to True
- [`node_id`](../../../reference/api#django_components.BaseNode.node_id): A unique identifier for this node instance
- [`flags`](../../../reference/api#django_components.BaseNode.flags): Dictionary of flag values (e.g. `{"required": True}`)
- [`params`](../../../reference/api#django_components.BaseNode.params): List of raw parameters passed to the tag
- [`nodelist`](../../../reference/api#django_components.BaseNode.nodelist): The template nodes between the start and end tags
- [`contents`](../../../reference/api#django_components.BaseNode.contents): The raw contents between the start and end tags
- [`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.

View file

@ -57,6 +57,7 @@ The Render API includes:
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
- 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.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
@ -337,17 +338,18 @@ class Table(Component):
If the component is rendered with [`{% component %}`](../../../reference/template_tags#component) template tag,
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
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.outer_context`](../../../reference/api/#django_components.Component.outer_context) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
```django
{% with abc=123 %}
{{ abc }} {# <--- This is in outer context #}
{% component "my_component" / %}
{% endwith %}
```
```django
{% with abc=123 %}
{{ abc }} {# <--- This is in outer context #}
{% component "my_component" / %}
{% endwith %}
```
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).
@ -360,3 +362,40 @@ class MyComponent(Component):
else:
# 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).

View file

@ -728,7 +728,7 @@ with extra metadata:
- [`component_name`](../../../reference/api#django_components.Slot.component_name)
- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
- [`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)
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.
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
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) tag
or in the component's Python code. See an example in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) and
[`{% 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):
@ -761,7 +784,6 @@ slot = Slot(
# Optional
component_name="table",
slot_name="name",
source="python",
extra={},
)
@ -771,6 +793,8 @@ slot.slot_name = "name"
slot.extra["foo"] = "bar"
```
Read more in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
### Slot contents
Whether you create a slot from a function, a string, or from the [`{% fill %}`](../../../reference/template_tags#fill) tags,

View file

@ -47,6 +47,10 @@
options:
show_if_no_docstring: true
::: django_components.ComponentNode
options:
show_if_no_docstring: true
::: django_components.ComponentRegistry
options:
show_if_no_docstring: true
@ -83,6 +87,14 @@
options:
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
options:
show_if_no_docstring: true
@ -111,6 +123,10 @@
options:
show_if_no_docstring: true
::: django_components.SlotNode
options:
show_if_no_docstring: true
::: django_components.SlotRef
options:
show_if_no_docstring: true

View file

@ -204,6 +204,7 @@ name | type | description
`slot_is_default` | `bool` | Whether the slot is default
`slot_is_required` | `bool` | Whether the slot is required
`slot_name` | `str` | The name of the `{% slot %}` tag
`slot_node` | `SlotNode` | The node instance of the `{% slot %}` tag
## Objects

View file

@ -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)
decorator.
The `{% component %}` tag takes:
The [`{% component %}`](../template_tags#component) tag takes:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
@ -92,7 +92,8 @@ The component name must be a string literal.
### Inserting slot fills
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
{% component "my_table" rows=rows headers=headers %}
@ -102,7 +103,7 @@ If the component defined any [slots](../concepts/fundamentals/slots.md), you can
{% 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),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
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
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.
**Args:**
- `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
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
@ -266,7 +268,7 @@ Fill:
### Using 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
{% component "button" %}
@ -280,7 +282,7 @@ use `{% fill %}` with `name` set to `"default"`:
### Slot fills from Python
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
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
{% component "table" %}
@ -306,7 +308,7 @@ Then pass the slot to the `{% fill %}` tag:
!!! 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.
```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.
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
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).
✅ 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.
[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
- `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)
- `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.
@ -527,8 +532,8 @@ class Parent(Component):
### Slot data
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
tag via fill's `data` kwarg:
Any extra kwargs will be considered as slot data, and will be accessible
in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
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
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
the fill's `fallback` kwarg.
This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
using the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
```djc_py

View file

@ -18,6 +18,7 @@ from django_components.util.command import (
from django_components.component import (
Component,
ComponentInput,
ComponentNode,
ComponentVars,
all_components,
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.library import TagProtectedError
from django_components.node import BaseNode, template_tag
from django_components.provide import ProvideNode
from django_components.slots import (
FillNode,
Slot,
SlotContent,
SlotContext,
SlotFallback,
SlotFunc,
SlotInput,
SlotNode,
SlotRef,
SlotResult,
)
@ -103,6 +107,7 @@ __all__ = [
"ComponentInput",
"ComponentMediaInput",
"ComponentMediaInputPath",
"ComponentNode",
"ComponentRegistry",
"ComponentVars",
"ComponentView",
@ -115,6 +120,7 @@ __all__ = [
"DynamicComponent",
"Empty",
"ExtensionComponentConfig",
"FillNode",
"format_attributes",
"get_component_by_class_id",
"get_component_dirs",
@ -131,6 +137,7 @@ __all__ = [
"OnComponentUnregisteredContext",
"OnRegistryCreatedContext",
"OnRegistryDeletedContext",
"ProvideNode",
"register",
"registry",
"RegistrySettings",
@ -142,6 +149,7 @@ __all__ = [
"SlotFallback",
"SlotFunc",
"SlotInput",
"SlotNode",
"SlotRef",
"SlotResult",
"TagFormatterABC",

View file

@ -1941,6 +1941,7 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
deps_strategy: Optional[DependenciesStrategy] = None,
request: Optional[HttpRequest] = None,
node: Optional["ComponentNode"] = None,
id: Optional[str] = None,
):
# 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.outer_context: Optional[Context] = outer_context
self.registry = default(registry, registry_)
self.node = node
extensions._init_component_instance(self)
@ -2346,6 +2348,51 @@ class Component(metaclass=ComponentMeta):
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`
is_filled: SlotIsFilled
"""
@ -2535,6 +2582,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
**response_kwargs: Any,
) -> HttpResponse:
"""
@ -2603,6 +2651,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
return cls.response_class(content, **response_kwargs)
@ -2623,6 +2672,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
"""
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`
registry=registry,
registered_name=registered_name,
node=node,
)
# This is the internal entrypoint for the render function
@ -2846,6 +2897,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
component_name = _get_component_name(cls, registered_name)
@ -2862,6 +2914,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
@classmethod
@ -2877,6 +2930,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
######################################
# 1. Handle inputs
@ -2929,6 +2983,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
# Allow plugins to modify or validate the inputs
@ -3316,7 +3371,7 @@ class ComponentNode(BaseNode):
[`@register()`](./api.md#django_components.register)
decorator.
The `{% component %}` tag takes:
The [`{% component %}`](../template_tags#component) tag takes:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
@ -3333,7 +3388,8 @@ class ComponentNode(BaseNode):
### Inserting slot fills
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
{% component "my_table" rows=rows headers=headers %}
@ -3343,7 +3399,7 @@ class ComponentNode(BaseNode):
{% 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),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
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
components by their registered names:
@ -3417,6 +3473,8 @@ class ComponentNode(BaseNode):
nodelist: Optional[NodeList] = None,
node_id: Optional[str] = None,
contents: Optional[str] = None,
template_name: Optional[str] = None,
template_component: Optional[Type["Component"]] = None,
) -> None:
super().__init__(
params=params,
@ -3424,6 +3482,8 @@ class ComponentNode(BaseNode):
nodelist=nodelist,
node_id=node_id,
contents=contents,
template_name=template_name,
template_component=template_component,
)
self.name = name
@ -3499,6 +3559,7 @@ class ComponentNode(BaseNode):
registered_name=self.name,
outer_context=context,
registry=self.registry,
node=self,
)
return output

View file

@ -28,7 +28,7 @@ from django_components.util.routing import URLRoute
if TYPE_CHECKING:
from django_components import Component
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)
@ -155,6 +155,8 @@ class OnSlotRenderedContext(NamedTuple):
"""The Slot instance that was rendered"""
slot_name: str
"""The name of the `{% slot %}` tag"""
slot_node: "SlotNode"
"""The node instance of the `{% slot %}` tag"""
slot_is_required: bool
"""Whether the slot is required"""
slot_is_default: bool
@ -744,6 +746,26 @@ class ComponentExtension(metaclass=ExtensionMeta):
# Append a comment to the slot's rendered output
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

View file

@ -1,7 +1,7 @@
import functools
import inspect
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.base import Node, NodeList, Parser, Token
@ -15,6 +15,9 @@ from django_components.util.template_tag import (
validate_params,
)
if TYPE_CHECKING:
from django_components.component import Component
# 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)
# #####################################
tag: str
tag: ClassVar[str]
"""
The tag name.
E.g. `"component"` or `"slot"` will make this class match
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.
E.g. `"endcomponent"` or `"endslot"` will make this class match
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.
So instead of `{% component %} ... {% endcomponent %}`, you'd use only
`{% 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 %}`.
```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:
@ -303,6 +351,133 @@ class BaseNode(Node, metaclass=NodeMeta):
"""
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
# #####################################
@ -314,12 +489,16 @@ class BaseNode(Node, metaclass=NodeMeta):
nodelist: Optional[NodeList] = None,
node_id: Optional[str] = None,
contents: Optional[str] = None,
template_name: Optional[str] = None,
template_component: Optional[Type["Component"]] = None,
):
self.params = params
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
self.nodelist = nodelist or NodeList()
self.node_id = node_id or gen_id()
self.contents = contents
self.template_name = template_name
self.template_component = template_component
def __repr__(self) -> str:
return (
@ -329,7 +508,21 @@ class BaseNode(Node, metaclass=NodeMeta):
@property
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 = []
for flag, value in self.flags.items():
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).
"""
# NOTE: Avoids circular import
from django_components.template import get_component_from_origin
tag_id = gen_id()
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,
flags=tag.flags,
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,
)

View file

@ -12,8 +12,11 @@ from django_components.util.misc import gen_id
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.
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
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).
Do this

View file

@ -265,14 +265,33 @@ class Slot(Generic[TSlotData]):
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'`),
or Python (`'python'`).
If the slot was created from a [`{% fill %}`](../template_tags#fill) tag,
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.
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)
"""
@ -494,7 +513,7 @@ class SlotIsFilled(dict):
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.
[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
- `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)
- `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.
@ -550,8 +569,8 @@ class SlotNode(BaseNode):
### Slot data
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
tag via fill's `data` kwarg:
Any extra kwargs will be considered as slot data, and will be accessible
in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
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
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
the fill's `fallback` kwarg.
This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
using the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
```djc_py
@ -926,6 +945,7 @@ class SlotNode(BaseNode):
component_id=component_id,
slot=slot,
slot_name=slot_name,
slot_node=self,
slot_is_required=is_required,
slot_is_default=is_default,
result=output,
@ -968,15 +988,16 @@ class SlotNode(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.
**Args:**
- `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
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
@ -1061,7 +1082,7 @@ class FillNode(BaseNode):
### Using 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
{% component "button" %}
@ -1075,7 +1096,7 @@ class FillNode(BaseNode):
### Slot fills from Python
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
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
{% component "table" %}
@ -1101,7 +1122,7 @@ class FillNode(BaseNode):
!!! 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.
```django
@ -1395,7 +1416,7 @@ def resolve_fills(
contents=contents,
data_var=None,
fallback_var=None,
source="template",
fill_node=component_node,
)
# The content has fills
@ -1405,14 +1426,15 @@ def resolve_fills(
for fill in maybe_fills:
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
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):
# Make a copy of the Slot instance and set it to `source="template"`,
# so it behaves the same as if the content was written inside the `{% fill %}` tag.
# 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")
# Make a copy of the Slot instance and set its `fill_node`.
slot_fill = dataclass_replace(fill.body, fill_node=fill.fill)
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 %}`
else:
slot_fill = _nodelist_to_slot(
@ -1423,7 +1445,7 @@ def resolve_fills(
data_var=fill.data_var,
fallback_var=fill.fallback_var,
extra_context=fill.extra_context,
source="template",
fill_node=fill.fill,
)
slots[fill.name] = slot_fill
@ -1497,14 +1519,14 @@ def normalize_slot_fills(
used_slot_name = content.slot_name or slot_name
used_nodelist = content.nodelist
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()
else:
used_component_name = component_name
used_slot_name = slot_name
used_nodelist = None
used_contents = content_func
used_source = "python"
used_fill_node = None
used_extra = {}
slot = Slot(
@ -1513,7 +1535,7 @@ def normalize_slot_fills(
component_name=used_component_name,
slot_name=used_slot_name,
nodelist=used_nodelist,
source=used_source,
fill_node=used_fill_node,
extra=used_extra,
)
@ -1544,7 +1566,7 @@ def _nodelist_to_slot(
data_var: Optional[str] = None,
fallback_var: Optional[str] = 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,
) -> Slot:
if data_var:
@ -1638,7 +1660,7 @@ def _nodelist_to_slot(
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
# So we need to special-case this.
contents=default(contents, ""),
source=default(source, "python"),
fill_node=default(fill_node, None),
extra=default(extra, {}),
)

View file

@ -3,6 +3,7 @@ Tests focusing on the Component class.
For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
import os
import re
from typing import Any, NamedTuple
@ -17,6 +18,7 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import (
Component,
ComponentRegistry,
ComponentView,
Slot,
SlotInput,
@ -598,6 +600,110 @@ class TestComponentRenderAPI:
assert comp.slots == {} # 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
class TestComponentTemplateVars:

View file

@ -6,7 +6,7 @@ from django.http import HttpRequest, HttpResponse
from django.template import Context
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.component_registry import ComponentRegistry
from django_components.extension import (
@ -140,6 +140,13 @@ class RenderExtension(ComponentExtension):
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):
class TempComponent(Component):
template = "Hello {{ name }}!"
@ -341,13 +348,15 @@ class TestExtensionHooks:
# Render the component with some args and kwargs
test_context = Context({"foo": "bar"})
TestComponent.render(
rendered = TestComponent.render(
context=test_context,
args=("arg1", "arg2"),
kwargs={"name": "Test"},
slots={"content": "Some content"},
)
assert rendered == "Hello Some content!"
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
# Verify on_slot_rendered was called with correct args
@ -359,10 +368,25 @@ class TestExtensionHooks:
assert slot_call.component_id == "ca1bc3e"
assert isinstance(slot_call.slot, Slot)
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_default is True
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
class TestExtensionViews:

View file

@ -1,12 +1,15 @@
import inspect
import os
import re
from typing import cast
import pytest
from django.template import Context, Template
from django.template.base import TextNode, VariableNode
from django.template.defaulttags import IfNode, LoremNode
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.templatetags import component_tags
from django_components.util.tag_parser import TagAttr
@ -849,7 +852,14 @@ class TestSignatureBasedValidation:
@force_signature_validation
def render(self, context: Context, name: str, **kwargs) -> str:
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}!"
# Case 1 - Node with end tag and NOT self-closing
@ -864,7 +874,7 @@ class TestSignatureBasedValidation:
template1 = Template(template_str1)
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 isinstance(params1[0], TagAttr)
# NOTE: The comment node is not included in the nodelist
@ -879,6 +889,8 @@ class TestSignatureBasedValidation:
assert isinstance(nodelist1[7], TextNode)
assert contents1 == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501
assert node_id1 == "a1bc3e"
assert template_name1 == '<unknown source>'
assert template_component1 is None
captured = None # Reset captured
@ -890,12 +902,14 @@ class TestSignatureBasedValidation:
template2 = Template(template_str2)
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 isinstance(params2[0], TagAttr) # type: ignore
assert len(nodelist2) == 0 # type: ignore
assert contents2 is None # 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
@ -906,7 +920,7 @@ class TestSignatureBasedValidation:
@force_signature_validation
def render(self, context: Context, name: str, **kwargs) -> str:
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}!"
TestNodeWithoutEndTag.register(component_tags.register)
@ -918,12 +932,38 @@ class TestSignatureBasedValidation:
template3 = Template(template_str3)
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 isinstance(params3[0], TagAttr) # type: ignore
assert len(nodelist3) == 0 # type: ignore
assert contents3 is None # 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
TestNodeWithEndTag.unregister(component_tags.register)

View file

@ -12,7 +12,8 @@ from django.template.base import NodeList, TextNode
from pytest_django.asserts import assertHTMLEqual
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 .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
@ -431,7 +432,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "SimpleComponent"
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 == {}
first_nodelist: NodeList = first_slot_func.nodelist
@ -465,7 +466,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "SimpleComponent"
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.nodelist is None
@ -495,7 +496,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "SimpleComponent"
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.nodelist is None
@ -530,7 +531,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "test"
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 == {}
first_nodelist: NodeList = first_slot_func.nodelist
@ -571,7 +572,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "test"
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 == {}
first_nodelist: NodeList = first_slot_func.nodelist
@ -616,7 +617,7 @@ class TestSlot:
assert isinstance(first_slot_func, Slot)
assert first_slot_func.component_name == "test"
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.nodelist is None