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 ```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

View file

@ -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.

View file

@ -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).

View file

@ -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,

View file

@ -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

View file

@ -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

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) [`@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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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,
) )

View file

@ -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

View file

@ -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, {}),
) )

View file

@ -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:

View file

@ -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:

View file

@ -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)

View file

@ -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