feat: Component.args/kwargs/slots and {{ component_vars.args/kwargs/s… (#1205)

* feat: Component.args/kwargs/slots and {{ component_vars.args/kwargs/slots }}

* docs: fix typo in changelog
This commit is contained in:
Juro Oravec 2025-05-24 23:24:34 +02:00 committed by GitHub
parent d514694788
commit e054a68715
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1262 additions and 217 deletions

View file

@ -4,6 +4,17 @@
⚠️ Major release ⚠️ - Please test thoroughly before / after upgrading.
Summary:
- Overhauled typing system
- Middleware removed, no longer needed
- `get_template_data()` is the new canonical way to define template data
- Slots API polished and prepared for v1.
- Merged `Component.Url` with `Component.View`
- Added `Component.args`, `Component.kwargs`, `Component.slots`
- Added `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}`
- And lot more...
#### 🚨📢 BREAKING CHANGES
**Middleware**
@ -68,7 +79,7 @@
text: str
```
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/0.140/concepts/fundamentals/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type.
@ -434,6 +445,66 @@
{% endfill %}
```
- The template variable `{{ component_vars.is_filled }}` is now deprecated. Will be removed in v1. Use `{{ component_vars.slots }}` instead.
Before:
```django
{% if component_vars.is_filled.footer %}
<div>
{% slot "footer" / %}
</div>
{% endif %}
```
After:
```django
{% if component_vars.slots.footer %}
<div>
{% slot "footer" / %}
</div>
{% endif %}
```
NOTE: `component_vars.is_filled` automatically escaped slot names, so that even slot names that are
not valid python identifiers could be set as slot names. `component_vars.slots` no longer does that.
- Component attribute `Component.is_filled` is now deprecated. Will be removed in v1. Use `Component.slots` instead.
Before:
```py
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
if self.is_filled.footer:
color = "red"
else:
color = "blue"
return {
"color": color,
}
```
After:
```py
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
if "footer" in slots:
color = "red"
else:
color = "blue"
return {
"color": color,
}
```
NOTE: `Component.is_filled` automatically escaped slot names, so that even slot names that are
not valid python identifiers could be set as slot names. `Component.slots` no longer does that.
#### Feat
- New method to render template variables - `get_template_data()`
@ -476,7 +547,7 @@
This practically brings back input validation, because the instantiation of the types
will raise an error if the inputs are not valid.
Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/)
Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/)
- Render emails or other non-browser HTML with new "dependencies strategies"
@ -525,6 +596,57 @@
See [Dependencies rendering](https://django-components.github.io/django-components/0.140/concepts/advanced/rendering_js_css/) for more info.
- New `Component.args`, `Component.kwargs`, `Component.slots` attributes available on the component class itself.
These attributes are the same as the ones available in `Component.get_template_data()`.
You can use these in other methods like `Component.on_render_before()` or `Component.on_render_after()`.
```py
from django_components import Component, SlotInput
class Table(Component):
class Args(NamedTuple):
page: int
class Kwargs(NamedTuple):
per_page: int
class Slots(NamedTuple):
content: SlotInput
def on_render_before(self, context: Context, template: Template) -> None:
assert self.args.page == 123
assert self.kwargs.per_page == 10
content_html = self.slots.content()
```
Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes
if defined, or plain lists / dictionaries otherwise.
- New template variables `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}`
These attributes are the same as the ones available in `Component.get_template_data()`.
```django
{# Typed #}
{% if component_vars.args.page == 123 %}
<div>
{% slot "content" / %}
</div>
{% endif %}
{# Untyped #}
{% if component_vars.args.0 == 123 %}
<div>
{% slot "content" / %}
</div>
{% endif %}
```
Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes
if defined, or plain lists / dictionaries otherwise.
- `get_component_url()` now optionally accepts `query` and `fragment` arguments.
```py

View file

@ -237,9 +237,9 @@ class Table(Component):
assert self.id == "djc1A2b3c"
# Access component's inputs and slots
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
footer_slot = self.slots["footer"]
some_var = self.input.context["some_var"]
# Access the request object and Django's context processors, if available
@ -390,7 +390,7 @@ class Header(Component):
### Input validation and static type hints
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/).
To opt-in to input validation, define types for component's args, kwargs, slots, and more:

View file

@ -67,7 +67,7 @@ and so `selected_items` will be set to `[1, 2, 3]`.
!!! warning
When [typing](../advanced/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
When [typing](../fundamentals/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
you may be inclined to define the defaults in the classes.

View file

@ -152,23 +152,23 @@ The difference is that:
## Accessing component inputs
The component inputs are available in two ways:
The component inputs are available in 3 ways:
1. **Function arguments (recommended)**
### Function arguments
The data methods receive the inputs as parameters, which you can access directly.
The data methods receive the inputs as parameters directly.
```python
class ProfileCard(Component):
def get_template_data(self, args, kwargs, slots, context):
```python
class ProfileCard(Component):
# Access inputs directly as parameters
def get_template_data(self, args, kwargs, slots, context):
return {
"user_id": user_id,
"show_details": show_details,
"user_id": args[0],
"show_details": kwargs["show_details"],
}
```
```
!!! info
!!! info
By default, the `args` parameter is a list, while `kwargs` and `slots` are dictionaries.
@ -178,28 +178,92 @@ The component inputs are available in two ways:
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
the respective inputs will be given as instances of these classes.
Learn more about [Component typing](../../advanced/typing_and_validation).
Learn more about [Component typing](../../fundamentals/typing_and_validation).
2. **`self.input` property**
The data methods receive only the main inputs. There are additional settings that may be passed
to components. If you need to access these, you can do so via the [`self.input`](../../../reference/api/#django_components.Component.input) property.
The `input` property contains all the inputs passed to the component (instance of [`ComponentInput`](../../../reference/api/#django_components.ComponentInput)).
This includes:
- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
- [`input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs) - Dictionary of keyword arguments
- [`input.slots`](../../../reference/api/#django_components.ComponentInput.slots) - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
- [`input.context`](../../../reference/api/#django_components.ComponentInput.context) - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
- [`input.type`](../../../reference/api/#django_components.ComponentInput.type) - The type of the component (document, fragment)
- [`input.render_dependencies`](../../../reference/api/#django_components.ComponentInput.render_dependencies) - Whether to render dependencies (CSS, JS)
For more details, see [Component inputs](../render_api/#component-inputs).
```python
```py
class ProfileCard(Component):
class Args(NamedTuple):
user_id: int
class Kwargs(NamedTuple):
show_details: bool
# Access inputs directly as parameters
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
return {
"user_id": args.user_id,
"show_details": kwargs.show_details,
}
```
### `args`, `kwargs`, `slots` properties
In other methods, you can access the inputs via
[`self.args`](../../../reference/api/#django_components.Component.args),
[`self.kwargs`](../../../reference/api/#django_components.Component.kwargs),
and [`self.slots`](../../../reference/api/#django_components.Component.slots) properties:
```py
class ProfileCard(Component):
def on_render_before(self, *args, **kwargs):
# Access inputs via self.args, self.kwargs, self.slots
self.args[0]
self.kwargs.get("show_details", False)
self.slots["footer"]
```
!!! info
These properties work the same way as `args`, `kwargs`, and `slots` parameters in the data methods:
By default, the `args` property is a list, while `kwargs` and `slots` are dictionaries.
If you add typing to your component with
[`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
the respective inputs will be given as instances of these classes.
Learn more about [Component typing](../../fundamentals/typing_and_validation).
```py
class ProfileCard(Component):
class Args(NamedTuple):
user_id: int
class Kwargs(NamedTuple):
show_details: bool
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
return {
"user_id": self.args.user_id,
"show_details": self.kwargs.show_details,
}
```
### `input` property (low-level)
The previous two approaches allow you to access only the most important inputs.
There are additional settings that may be passed to components.
If you need to access these, you can use [`self.input`](../../../reference/api/#django_components.Component.input) property
for a low-level access to all the inputs.
The `input` property contains all the inputs passed to the component (instance of [`ComponentInput`](../../../reference/api/#django_components.ComponentInput)).
This includes:
- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
- [`input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs) - Dictionary of keyword arguments
- [`input.slots`](../../../reference/api/#django_components.ComponentInput.slots) - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
- [`input.context`](../../../reference/api/#django_components.ComponentInput.context) - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
- [`input.type`](../../../reference/api/#django_components.ComponentInput.type) - The type of the component (document, fragment)
- [`input.render_dependencies`](../../../reference/api/#django_components.ComponentInput.render_dependencies) - Whether to render dependencies (CSS, JS)
For more details, see [Component inputs](../render_api/#component-inputs).
```python
class ProfileCard(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access positional arguments
user_id = self.input.args[0] if self.input.args else None
@ -215,12 +279,14 @@ The component inputs are available in two ways:
"user_id": user_id,
"show_details": show_details,
}
```
```
!!! info
!!! info
Unlike the parameters passed to the data methods, the `args`, `kwargs`, and `slots` in `self.input` property are always lists and dictionaries,
regardless of whether you added typing to your component.
regardless of whether you added typing classes to your component (like [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots)).
## Default values
@ -279,8 +345,11 @@ class ProfileCard(Component):
All three data methods have access to the Component's [Render API](./render_api.md), which includes:
- [`self.id`](./render_api/#component-id) - The unique ID for the current render call
- [`self.args`](./render_api/#args) - The positional arguments for the current render call
- [`self.kwargs`](./render_api/#kwargs) - The keyword arguments for the current render call
- [`self.slots`](./render_api/#slots) - The slots for the current render call
- [`self.input`](./render_api/#component-inputs) - All the component inputs
- [`self.id`](./render_api/#component-id) - The unique ID for the current render call
- [`self.request`](./render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](./render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](./render_api/#provide-inject) - Inject data into the component
@ -298,7 +367,7 @@ and then add type hints to the data methods.
This will also validate the inputs at runtime, as the type classes will be instantiated with the inputs.
Read more about [Component typing](../../advanced/typing_and_validation).
Read more about [Component typing](../../fundamentals/typing_and_validation).
```python
from typing import NamedTuple, Optional

View file

@ -20,24 +20,21 @@ Example:
```python
class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
def on_render_before(self, context, template):
# Access component's ID
assert self.id == "c1A2b3c"
# Access component's inputs, slots and context
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
footer_slot = self.slots["footer"]
some_var = self.input.context["some_var"]
def get_template_data(self, args, kwargs, slots, context):
# Access the request object and Django's context processors, if available
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {
"variable": variable,
}
rendered = Table.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
@ -49,42 +46,159 @@ rendered = Table.render(
The Render API includes:
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
- [`self.args`](../render_api/#args) - The positional arguments for the current render call
- [`self.kwargs`](../render_api/#kwargs) - The keyword arguments for the current render call
- [`self.slots`](../render_api/#slots) - The slots for the current render call
- [`self.input`](../render_api/#component-inputs) - All the component inputs
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
- [`self.request`](../render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](../render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
## Component ID
## Args
Component ID (or render ID) is a unique identifier for the current render call.
The `args` argument as passed to
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
That means that if you call [`Component.render()`](../../../reference/api#django_components.Component.render)
multiple times, the ID will be different for each call.
If you defined the [`Component.Args`](../../../reference/api/#django_components.Component.Args) class,
then the [`Component.args`](../../../reference/api/#django_components.Component.args) property will return an instance of that class.
It is available as [`self.id`](../../../reference/api#django_components.Component.id).
Otherwise, `args` will be a plain list.
The ID is a 7-letter alphanumeric string in the format `cXXXXXX`,
where `XXXXXX` is a random string of 6 alphanumeric characters (case-sensitive).
Raises `RuntimeError` if accessed outside of rendering execution.
E.g. `c1a2b3c`.
**Example:**
A single render ID has a chance of collision 1 in 57 billion. However, due to birthday paradox, the chance of collision increases to 1% when approaching ~33K render IDs.
Thus, there is currently a soft-cap of ~30K components rendered on a single page.
If you need to expand this limit, please open an issue on GitHub.
With `Args` class:
```python
from django_components import Component
class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access component's ID
assert self.id == "c1A2b3c"
class Args(NamedTuple):
page: int
per_page: int
def on_render_before(self, context: Context, template: Template) -> None:
assert self.args.page == 123
assert self.args.per_page == 10
rendered = Table.render(
args=[123, 10],
)
```
Without `Args` class:
```python
from django_components import Component
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert self.args[0] == 123
assert self.args[1] == 10
```
## Kwargs
The `kwargs` argument as passed to
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
If you defined the [`Component.Kwargs`](../../../reference/api/#django_components.Component.Kwargs) class,
then the [`Component.kwargs`](../../../reference/api/#django_components.Component.kwargs) property will return an instance of that class.
Otherwise, `kwargs` will be a plain dictionary.
Raises `RuntimeError` if accessed outside of rendering execution.
**Example:**
With `Kwargs` class:
```python
from django_components import Component
class Table(Component):
class Kwargs(NamedTuple):
page: int
per_page: int
def on_render_before(self, context: Context, template: Template) -> None:
assert self.kwargs.page == 123
assert self.kwargs.per_page == 10
rendered = Table.render(
kwargs={"page": 123, "per_page": 10},
)
```
Without `Kwargs` class:
```python
from django_components import Component
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert self.kwargs["page"] == 123
assert self.kwargs["per_page"] == 10
```
## Slots
The `slots` argument as passed to
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
If you defined the [`Component.Slots`](../../../reference/api/#django_components.Component.Slots) class,
then the [`Component.slots`](../../../reference/api/#django_components.Component.slots) property will return an instance of that class.
Otherwise, `slots` will be a plain dictionary.
Raises `RuntimeError` if accessed outside of rendering execution.
**Example:**
With `Slots` class:
```python
from django_components import Component, Slot, SlotInput
class Table(Component):
class Slots(NamedTuple):
header: SlotInput
footer: SlotInput
def on_render_before(self, context: Context, template: Template) -> None:
assert isinstance(self.slots.header, Slot)
assert isinstance(self.slots.footer, Slot)
rendered = Table.render(
slots={
"header": "MY_HEADER",
"footer": lambda ctx: "FOOTER: " + ctx.data["user_id"],
},
)
```
Without `Slots` class:
```python
from django_components import Component, Slot, SlotInput
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert isinstance(self.slots["header"], Slot)
assert isinstance(self.slots["footer"], Slot)
```
## Component inputs
All the component inputs are captured and available as [`self.input`](../../../reference/api/#django_components.Component.input).
You can access the most important inputs via [`self.args`](../render_api/#args),
[`self.kwargs`](../render_api/#kwargs),
and [`self.slots`](../render_api/#slots) properties.
There are additional settings that may be passed to components.
If you need to access these, you can use [`self.input`](../../../reference/api/#django_components.Component.input) property
for a low-level access to all the inputs passed to the component.
[`self.input`](../../../reference/api/#django_components.Component.input) ([`ComponentInput`](../../../reference/api/#django_components.ComponentInput)) has the mostly the same fields as the input to [`Component.render()`](../../../reference/api/#django_components.Component.render). This includes:
@ -114,6 +228,33 @@ rendered = TestComponent.render(
)
```
## Component ID
Component ID (or render ID) is a unique identifier for the current render call.
That means that if you call [`Component.render()`](../../../reference/api#django_components.Component.render)
multiple times, the ID will be different for each call.
It is available as [`self.id`](../../../reference/api#django_components.Component.id).
The ID is a 7-letter alphanumeric string in the format `cXXXXXX`,
where `XXXXXX` is a random string of 6 alphanumeric characters (case-sensitive).
E.g. `c1a2b3c`.
A single render ID has a chance of collision 1 in 57 billion. However, due to birthday paradox, the chance of collision increases to 1% when approaching ~33K render IDs.
Thus, there is currently a soft-cap of ~30K components rendered on a single page.
If you need to expand this limit, please open an issue on GitHub.
```python
class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access component's ID
assert self.id == "c1A2b3c"
```
## Request object and context processors
Components have access to the request object and context processors data if the component was:

View file

@ -484,7 +484,7 @@ in component's [`Args`](../../../reference/api/#django_components.Component.Args
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes.
Read more on [Typing and validation](../../advanced/typing_and_validation).
Read more on [Typing and validation](../../fundamentals/typing_and_validation).
```python
from typing import NamedTuple, Optional

View file

@ -125,8 +125,8 @@ Thus, you can check where a slot was filled from by printing it out:
```python
class MyComponent(Component):
def on_render_before(self):
print(self.input.slots)
def on_render_before(self, *args, **kwargs):
print(self.slots)
```
might print:

View file

@ -227,9 +227,9 @@ class Table(Component):
assert self.id == "djc1A2b3c"
# Access component's inputs and slots
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
footer_slot = self.slots["footer"]
some_var = self.input.context["some_var"]
# Access the request object and Django's context processors, if available
@ -380,9 +380,9 @@ class Header(Component):
### Input validation and static type hints
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/).
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
To opt-in to input validation, define types for component's args, kwargs, slots:
```py
from typing import NamedTuple, Optional

View file

@ -169,28 +169,43 @@ class ComponentInput:
This object is available only during render under [`Component.input`](../api#django_components.Component.input).
Read more about the [Render API](../../concepts/fundamentals/render_api).
This class can be typed as:
"""
context: Context
"""
Django's [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
passed to `Component.render()`
"""
args: List
"""Positional arguments (as list) passed to `Component.render()`"""
kwargs: Dict
"""Keyword arguments (as dict) passed to `Component.render()`"""
slots: Dict[SlotName, Slot]
"""Slots (as dict) passed to `Component.render()`"""
deps_strategy: DependenciesStrategy
"""Dependencies strategy passed to `Component.render()`"""
# TODO_v1 - Remove, superseded by `deps_strategy`
type: DependenciesStrategy
"""Deprecated alias for `deps_strategy`."""
"""Deprecated. Will be removed in v1. Use `deps_strategy` instead."""
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies: bool
"""Deprecated. Instead use `deps_strategy="ignore"`."""
"""Deprecated. Will be removed in v1. Use `deps_strategy="ignore"` instead."""
@dataclass()
class MetadataItem:
render_id: str
args: Any
"""`args` as passed to `get_template_data()` - instance of `Component.Args` if set, otherwise plain list"""
kwargs: Any
"""`kwargs` as passed to `get_template_data()` - instance of `Component.Kwargs` if set, otherwise plain dict"""
slots: Any
"""`slots` as passed to `get_template_data()` - instance of `Component.Slots` if set, otherwise plain dict"""
input: ComponentInput
is_filled: Optional[SlotIsFilled]
"""Raw input as passed to `Component.render()`"""
# TODO_V1 - Remove, superseded by `Component.slots`
is_filled: SlotIsFilled
"""Dictionary describing which component slots are filled (`True`) or are not (`False`)."""
request: Optional[HttpRequest]
@ -199,15 +214,174 @@ class ComponentVars(NamedTuple):
Type for the variables available inside the component templates.
All variables here are scoped under `component_vars.`, so e.g. attribute
`is_filled` on this class is accessible inside the template as:
`kwargs` on this class is accessible inside the template as:
```django
{{ component_vars.is_filled }}
{{ component_vars.kwargs }}
```
"""
args: Any
"""
The `args` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
This is the same [`Component.args`](../api/#django_components.Component.args)
that's available on the component instance.
If you defined the [`Component.Args`](../api/#django_components.Component.Args) class,
then the `args` property will return an instance of that class.
Otherwise, `args` will be a plain list.
**Example:**
With `Args` class:
```djc_py
from django_components import Component, register
@register("table")
class Table(Component):
class Args(NamedTuple):
page: int
per_page: int
template = '''
<div>
<h1>Table</h1>
<p>Page: {{ component_vars.args.page }}</p>
<p>Per page: {{ component_vars.args.per_page }}</p>
</div>
'''
```
Without `Args` class:
```djc_py
from django_components import Component, register
@register("table")
class Table(Component):
template = '''
<div>
<h1>Table</h1>
<p>Page: {{ component_vars.args.0 }}</p>
<p>Per page: {{ component_vars.args.1 }}</p>
</div>
'''
```
"""
kwargs: Any
"""
The `kwargs` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
This is the same [`Component.kwargs`](../api/#django_components.Component.kwargs)
that's available on the component instance.
If you defined the [`Component.Kwargs`](../api/#django_components.Component.Kwargs) class,
then the `kwargs` property will return an instance of that class.
Otherwise, `kwargs` will be a plain dict.
**Example:**
With `Kwargs` class:
```djc_py
from django_components import Component, register
@register("table")
class Table(Component):
class Kwargs(NamedTuple):
page: int
per_page: int
template = '''
<div>
<h1>Table</h1>
<p>Page: {{ component_vars.kwargs.page }}</p>
<p>Per page: {{ component_vars.kwargs.per_page }}</p>
</div>
'''
```
Without `Kwargs` class:
```djc_py
from django_components import Component, register
@register("table")
class Table(Component):
template = '''
<div>
<h1>Table</h1>
<p>Page: {{ component_vars.kwargs.page }}</p>
<p>Per page: {{ component_vars.kwargs.per_page }}</p>
</div>
'''
```
"""
slots: Any
"""
The `slots` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
This is the same [`Component.slots`](../api/#django_components.Component.slots)
that's available on the component instance.
If you defined the [`Component.Slots`](../api/#django_components.Component.Slots) class,
then the `slots` property will return an instance of that class.
Otherwise, `slots` will be a plain dict.
**Example:**
With `Slots` class:
```djc_py
from django_components import Component, SlotInput, register
@register("table")
class Table(Component):
class Slots(NamedTuple):
footer: SlotInput
template = '''
<div>
{% component "pagination" %}
{% fill "footer" body=component_vars.slots.footer / %}
{% endcomponent %}
</div>
'''
```
Without `Slots` class:
```djc_py
from django_components import Component, SlotInput, register
@register("table")
class Table(Component):
template = '''
<div>
{% component "pagination" %}
{% fill "footer" body=component_vars.slots.footer / %}
{% endcomponent %}
</div>
'''
```
"""
# TODO_v1 - Remove, superseded by `component_vars.slots`
is_filled: Dict[str, bool]
"""
Deprecated. Will be removed in v1. Use [`component_vars.slots`](../template_vars#django_components.component.ComponentVars.slots) instead.
Note that `component_vars.slots` no longer escapes the slot names.
Dictonary describing which component slots are filled (`True`) or are not (`False`).
<i>New in version 0.70</i>
@ -234,7 +408,7 @@ class ComponentVars(NamedTuple):
"my_slot_filled": "my_slot" in slots
}
```
"""
""" # noqa: E501
# Descriptor to pass getting/setting of `template_name` onto `template_file`
@ -346,7 +520,7 @@ class Component(metaclass=ComponentMeta):
)
```
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
"""
Kwargs: Type = cast(Type, None)
@ -403,7 +577,7 @@ class Component(metaclass=ComponentMeta):
)
```
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
"""
Slots: Type = cast(Type, None)
@ -463,7 +637,7 @@ class Component(metaclass=ComponentMeta):
)
```
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info
@ -669,7 +843,7 @@ class Component(metaclass=ComponentMeta):
When you omit these classes, or set them to `None`, then the `args`, `kwargs`, and `slots`
parameters will be given as plain lists / dictionaries, unmodified.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
**Example:**
@ -786,7 +960,7 @@ class Component(metaclass=ComponentMeta):
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info
@ -939,7 +1113,7 @@ class Component(metaclass=ComponentMeta):
When you omit these classes, or set them to `None`, then the `args`, `kwargs`, and `slots`
parameters will be given as plain lists / dictionaries, unmodified.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
**Example:**
@ -1049,7 +1223,7 @@ class Component(metaclass=ComponentMeta):
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info
@ -1210,7 +1384,7 @@ class Component(metaclass=ComponentMeta):
When you omit these classes, or set them to `None`, then the `args`, `kwargs`, and `slots`
parameters will be given as plain lists / dictionaries, unmodified.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
**Example:**
@ -1319,7 +1493,7 @@ class Component(metaclass=ComponentMeta):
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info
@ -1703,14 +1877,14 @@ class Component(metaclass=ComponentMeta):
class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access component's inputs, slots and context
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
footer_slot = self.slots["footer"]
some_var = self.input.context["some_var"]
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
args=[123, "str"],
slots={"footer": "MY_SLOT"},
)
```
@ -1723,15 +1897,196 @@ class Component(metaclass=ComponentMeta):
return self._metadata_stack[-1].input
@property
def is_filled(self) -> SlotIsFilled:
def args(self) -> Any:
"""
Dictionary describing which slots have or have not been filled.
This attribute is available for use only within the template as `{{ component_vars.is_filled.slot_name }}`,
and within `on_render_before` and `on_render_after` hooks.
The `args` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
Raises `RuntimeError` if accessed outside of rendering execution.
This is part of the [Render API](../../concepts/fundamentals/render_api).
If you defined the [`Component.Args`](../api/#django_components.Component.Args) class,
then the `args` property will return an instance of that class.
Otherwise, `args` will be a plain list.
**Example:**
With `Args` class:
```python
from django_components import Component
class Table(Component):
class Args(NamedTuple):
page: int
per_page: int
def on_render_before(self, context: Context, template: Template) -> None:
assert self.args.page == 123
assert self.args.per_page == 10
rendered = Table.render(
args=[123, 10],
)
```
Without `Args` class:
```python
from django_components import Component
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert self.args[0] == 123
assert self.args[1] == 10
```
"""
if not len(self._metadata_stack):
raise RuntimeError(
f"{self.name}: Tried to access Component's `args` attribute while outside of rendering execution"
)
# NOTE: Input is managed as a stack, so if `render` is called within another `render`,
# the propertes below will return only the inner-most state.
return self._metadata_stack[-1].args
@property
def kwargs(self) -> Any:
"""
The `kwargs` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
Raises `RuntimeError` if accessed outside of rendering execution.
This is part of the [Render API](../../concepts/fundamentals/render_api).
If you defined the [`Component.Kwargs`](../api/#django_components.Component.Kwargs) class,
then the `kwargs` property will return an instance of that class.
Otherwise, `kwargs` will be a plain dict.
**Example:**
With `Kwargs` class:
```python
from django_components import Component
class Table(Component):
class Kwargs(NamedTuple):
page: int
per_page: int
def on_render_before(self, context: Context, template: Template) -> None:
assert self.kwargs.page == 123
assert self.kwargs.per_page == 10
rendered = Table.render(
kwargs={
"page": 123,
"per_page": 10,
},
)
```
Without `Kwargs` class:
```python
from django_components import Component
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert self.kwargs["page"] == 123
assert self.kwargs["per_page"] == 10
```
"""
if not len(self._metadata_stack):
raise RuntimeError(
f"{self.name}: Tried to access Component's `kwargs` attribute while outside of rendering execution"
)
# NOTE: Input is managed as a stack, so if `render` is called within another `render`,
# the propertes below will return only the inner-most state.
return self._metadata_stack[-1].kwargs
@property
def slots(self) -> Any:
"""
The `slots` argument as passed to
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
Raises `RuntimeError` if accessed outside of rendering execution.
This is part of the [Render API](../../concepts/fundamentals/render_api).
If you defined the [`Component.Slots`](../api/#django_components.Component.Slots) class,
then the `slots` property will return an instance of that class.
Otherwise, `slots` will be a plain dict.
**Example:**
With `Slots` class:
```python
from django_components import Component, Slot, SlotInput
class Table(Component):
class Slots(NamedTuple):
header: SlotInput
footer: SlotInput
def on_render_before(self, context: Context, template: Template) -> None:
assert isinstance(self.slots.header, Slot)
assert isinstance(self.slots.footer, Slot)
rendered = Table.render(
slots={
"header": "MY_HEADER",
"footer": lambda ctx: "FOOTER: " + ctx.data["user_id"],
},
)
```
Without `Slots` class:
```python
from django_components import Component, Slot, SlotInput
class Table(Component):
def on_render_before(self, context: Context, template: Template) -> None:
assert isinstance(self.slots["header"], Slot)
assert isinstance(self.slots["footer"], Slot)
```
"""
if not len(self._metadata_stack):
raise RuntimeError(
f"{self.name}: Tried to access Component's `slots` attribute while outside of rendering execution"
)
# NOTE: Input is managed as a stack, so if `render` is called within another `render`,
# the propertes below will return only the inner-most state.
return self._metadata_stack[-1].slots
# TODO_v1 - Remove, superseded by `Component.slots`
@property
def is_filled(self) -> SlotIsFilled:
"""
Deprecated. Will be removed in v1. Use [`Component.slots`](../api/#django_components.Component.slots) instead.
Note that `Component.slots` no longer escapes the slot names.
Dictionary describing which slots have or have not been filled.
This attribute is available for use only within:
- The template as [`{{ component_vars.is_filled.slot_name }}`](../template_vars#django_components.component.ComponentVars.is_filled)
- Within [`on_render_before()`](../api/#django_components.Component.on_render_before)
and [`on_render_after()`](../api/#django_components.Component.on_render_after) hooks.
Raises `RuntimeError` if accessed outside of rendering execution.
""" # noqa: E501
if not len(self._metadata_stack):
raise RuntimeError(
f"{self.name}: Tried to access Component's `is_filled` attribute "
@ -2231,7 +2586,7 @@ class Component(metaclass=ComponentMeta):
[`Kwargs`](../api/#django_components.Component.Kwargs),
and [`Slots`](../api/#django_components.Component.Slots) classes.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
```python
from typing import NamedTuple, Optional
@ -2314,6 +2669,10 @@ class Component(metaclass=ComponentMeta):
deps_strategy: DependenciesStrategy = "document",
request: Optional[HttpRequest] = None,
) -> str:
######################################
# 1. Handle inputs
######################################
# Allow to pass down Request object via context.
# `context` may be passed explicitly via `Component.render()` and `Component.render_to_response()`,
# or implicitly via `{% component %}` tag.
@ -2335,6 +2694,7 @@ class Component(metaclass=ComponentMeta):
slots_dict = normalize_slot_fills(
to_dict(slots) if slots is not None else {},
escape_slots_content,
component_name=self.name,
)
# Use RequestContext if request is provided, so that child non-component template tags
# can access the request object too.
@ -2346,34 +2706,7 @@ class Component(metaclass=ComponentMeta):
if not isinstance(context, Context):
context = RequestContext(request, context) if request else Context(context)
# Required for compatibility with Django's {% extends %} tag
# See https://github.com/django-components/django-components/pull/859
context.render_context.push({BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())})
# By adding the current input to the stack, we temporarily allow users
# to access the provided context, slots, etc. Also required so users can
# call `self.inject()` from within `get_template_data()`.
#
# This is handled as a stack, as users can potentially call `component.render()`
# from within component hooks. Thus, then they do so, `component.id` will be the ID
# of the deepest-most call to `component.render()`.
render_id = COMP_ID_PREFIX + gen_id()
metadata = MetadataItem(
render_id=render_id,
input=ComponentInput(
context=context,
args=args_list,
kwargs=kwargs_dict,
slots=slots_dict,
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies=deps_strategy != "ignore",
),
is_filled=None,
request=request,
)
# Allow plugins to modify or validate the inputs
result_override = extensions.on_component_input(
@ -2391,10 +2724,16 @@ class Component(metaclass=ComponentMeta):
# The component rendering was short-circuited by an extension, skipping
# the rest of the rendering process. This may be for example a cached content.
if result_override is not None:
# Cleanup needs to be done even if short-circuited
context.render_context.pop()
return result_override
######################################
# 2. Prepare component state
######################################
# Required for compatibility with Django's {% extends %} tag
# See https://github.com/django-components/django-components/pull/859
context.render_context.push({BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())})
# We pass down the components the info about the component's parent.
# This is used for correctly resolving slot fills, correct rendering order,
# or CSS scoping.
@ -2448,14 +2787,59 @@ class Component(metaclass=ComponentMeta):
# Instead of passing the ComponentContext directly through the Context, the entry on the Context
# contains only a key to retrieve the ComponentContext from `component_context_cache`.
#
# This way, the flow is easier to debug. Because otherwise, if you try to print out
# or inspect the Context object, your screen is filled with the deeply nested ComponentContext objects.
# This way, the flow is easier to debug. Because otherwise, if you tried to print out
# or inspect the Context object, your screen would be filled with the deeply nested ComponentContext objects.
# But now, the printed Context may simply look like this:
# `[{ "True": True, "False": False, "None": None }, {"_DJC_COMPONENT_CTX": "c1A2b3c"}]`
component_context_cache[render_id] = component_ctx
# If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain
# dicts / lists.
args_inst = self.Args(*args_list) if self.Args is not None else args_list
kwargs_inst = self.Kwargs(**kwargs_dict) if self.Kwargs is not None else kwargs_dict
slots_inst = self.Slots(**slots_dict) if self.Slots is not None else slots_dict
# By adding the current input to the stack, we temporarily allow users
# to access the provided context, slots, etc. Also required so users can
# call `self.inject()` from within `get_template_data()`.
#
# This is handled as a stack, as users can potentially call `component.render()`
# from within component hooks. Thus, then they do so, `component.id` will be the ID
# of the deepest-most call to `component.render()`.
metadata = MetadataItem(
render_id=render_id,
# args, kwargs, slots as instances of `Args`, `Kwargs`, `Slots`
args=args_inst,
kwargs=kwargs_inst,
slots=slots_inst,
input=ComponentInput(
context=context,
# args, kwargs, slots are plain list / dicts
args=args_list,
kwargs=kwargs_dict,
slots=slots_dict,
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies=deps_strategy != "ignore",
),
# TODO_v1 - Remove, superseded by `Component.slots`
is_filled=SlotIsFilled(slots_dict),
request=request,
)
######################################
# 3. Call data methods
######################################
# Allow to access component input and metadata like component ID from within data methods
with self._with_metadata(metadata):
template_data, js_data, css_data = self._call_data_methods(args_list, kwargs_dict, slots_dict, context)
# Prepare context processors data, so we don't have to call `_with_metadata() later again
template_data, js_data, css_data = self._call_data_methods(
args_inst, kwargs_inst, slots_inst, context, args_list, kwargs_dict
)
# Prepare data for which we need the metadata context, so we don't have to call `_with_metadata() again
context_processors_data = self.context_processors_data
extensions.on_component_data(
@ -2478,15 +2862,17 @@ class Component(metaclass=ComponentMeta):
cache_component_css(self.__class__)
css_input_hash = cache_component_css_vars(self.__class__, css_data) if css_data else None
#############################################################################
# 4. Make Context copy
#
# NOTE: To support infinite recursion, we make a copy of the context.
# This way we don't have to call the whole component tree in one go recursively,
# but instead can render one component at a time.
#############################################################################
with _prepare_template(self, context, template_data, metadata) as template:
component_ctx.template_name = template.name
# For users, we expose boolean variables that they may check
# to see if given slot was filled, e.g.:
# `{% if variable > 8 and component_vars.is_filled.header %}`
is_filled = SlotIsFilled(slots_dict)
metadata.is_filled = is_filled
with context.update(
{
# Make data from context processors available inside templates
@ -2496,7 +2882,15 @@ class Component(metaclass=ComponentMeta):
# NOTE: Public API for variables accessible from within a component's template
# See https://github.com/django-components/django-components/issues/280#issuecomment-2081180940
"component_vars": ComponentVars(
is_filled=is_filled,
args=args_inst,
kwargs=kwargs_inst,
slots=slots_inst,
# TODO_v1 - Remove this, superseded by `component_vars.slots`
#
# For users, we expose boolean variables that they may check
# to see if given slot was filled, e.g.:
# `{% if variable > 8 and component_vars.is_filled.header %}`
is_filled=metadata.is_filled,
),
}
):
@ -2514,6 +2908,14 @@ class Component(metaclass=ComponentMeta):
# Cleanup
context.render_context.pop()
######################################
# 5. Render component
#
# NOTE: To support infinite recursion, we don't directly call `Template.render()`.
# Instead, we defer rendering of the component - we prepare a callback that will
# be called when the rendering process reaches this component.
######################################
# Instead of rendering component at the time we come across the `{% component %}` tag
# in the template, we defer rendering in order to scalably handle deeply nested components.
#
@ -2662,19 +3064,22 @@ class Component(metaclass=ComponentMeta):
return renderer
def _call_data_methods(self, args: Any, kwargs: Any, slots: Any, context: Context) -> Tuple[Dict, Dict, Dict]:
# If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain
# dicts / lists.
args_inst = self.Args(*args) if self.Args is not None else args
kwargs_inst = self.Kwargs(**kwargs) if self.Kwargs is not None else kwargs
slots_inst = self.Slots(**slots) if self.Slots is not None else slots
def _call_data_methods(
self,
args: Any,
kwargs: Any,
slots: Any,
context: Context,
# TODO_V2 - Remove `raw_args` and `raw_kwargs` in v2
raw_args: List,
raw_kwargs: Dict,
) -> Tuple[Dict, Dict, Dict]:
# Template data
maybe_template_data = self.get_template_data(args_inst, kwargs_inst, slots_inst, context)
maybe_template_data = self.get_template_data(args, kwargs, slots, context)
new_template_data = to_dict(default(maybe_template_data, {}))
# TODO_V2 - Remove this in v2
legacy_template_data = to_dict(default(self.get_context_data(*args, **kwargs), {}))
legacy_template_data = to_dict(default(self.get_context_data(*raw_args, **raw_kwargs), {}))
if legacy_template_data and new_template_data:
raise RuntimeError(
f"Component {self.name} has both `get_context_data()` and `get_template_data()` methods. "
@ -2684,11 +3089,11 @@ class Component(metaclass=ComponentMeta):
# TODO - Enable JS and CSS vars - expose, and document
# JS data
maybe_js_data = self.get_js_data(args_inst, kwargs_inst, slots_inst, context)
maybe_js_data = self.get_js_data(args, kwargs, slots, context)
js_data = to_dict(default(maybe_js_data, {}))
# CSS data
maybe_css_data = self.get_css_data(args_inst, kwargs_inst, slots_inst, context)
maybe_css_data = self.get_css_data(args, kwargs, slots, context)
css_data = to_dict(default(maybe_css_data, {}))
# Validate outputs

View file

@ -1,5 +1,5 @@
import inspect
from typing import Any, Dict, Optional, Type, Union, cast
from typing import Any, Optional, Type, Union, cast
from django.template import Context, Template
@ -99,35 +99,24 @@ class DynamicComponent(Component):
_is_dynamic_component = True
def get_template_data(
self,
args: Any,
kwargs: Any,
slots: Any,
context: Any,
) -> Dict:
registry: Optional[ComponentRegistry] = kwargs.pop("registry", None)
comp_name_or_class: Union[str, Type[Component]] = kwargs.pop("is", None)
if not comp_name_or_class:
raise TypeError(f"Component '{self.name}' is missing a required argument 'is'")
comp_class = self._resolve_component(comp_name_or_class, registry)
return {
"comp_class": comp_class,
"args": args,
"kwargs": kwargs,
}
# TODO: Replace combination of `on_render_before()` + `template` with single `on_render()`
#
# NOTE: The inner component is rendered in `on_render_before`, so that the `Context` object
# is already configured as if the inner component was rendered inside the template.
# E.g. the `_COMPONENT_CONTEXT_KEY` is set, which means that the child component
# will know that it's a child of this component.
def on_render_before(self, context: Context, template: Template) -> Context:
comp_class: type[Component] = context["comp_class"]
args = context["args"]
kwargs = context["kwargs"]
# Make a copy of kwargs so we pass to the child only the kwargs that are
# actually used by the child component.
cleared_kwargs = self.input.kwargs.copy()
# Resolve the component class
registry: Optional[ComponentRegistry] = cleared_kwargs.pop("registry", None)
comp_name_or_class: Union[str, Type[Component]] = cleared_kwargs.pop("is", None)
if not comp_name_or_class:
raise TypeError(f"Component '{self.name}' is missing a required argument 'is'")
comp_class = self._resolve_component(comp_name_or_class, registry)
comp = comp_class(
registered_name=self.registered_name,
@ -136,8 +125,8 @@ class DynamicComponent(Component):
)
output = comp.render(
context=self.input.context,
args=args,
kwargs=kwargs,
args=self.input.args,
kwargs=cleared_kwargs,
slots=self.input.slots,
# NOTE: Since we're accessing slots as `self.input.slots`, the content of slot functions
# was already escaped (if set so).
@ -145,6 +134,7 @@ class DynamicComponent(Component):
deps_strategy=self.input.deps_strategy,
)
# Set the output to the context so it can be accessed from within the template.
context["output"] = output
return context

View file

@ -401,18 +401,37 @@ class SlotFallback:
SlotRef = SlotFallback
name_escape_re = re.compile(r"[^\w]")
# TODO_v1 - Remove, superseded by `Component.slots` and `component_vars.slots`
class SlotIsFilled(dict):
"""
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
"""
def __init__(self, fills: Dict, *args: Any, **kwargs: Any) -> None:
escaped_fill_names = {_escape_slot_name(fill_name): True for fill_name in fills.keys()}
escaped_fill_names = {self._escape_slot_name(fill_name): True for fill_name in fills.keys()}
super().__init__(escaped_fill_names, *args, **kwargs)
def __missing__(self, key: Any) -> bool:
return False
def _escape_slot_name(self, name: str) -> str:
"""
Users may define slots with names which are invalid identifiers like 'my slot'.
But these cannot be used as keys in the template context, e.g. `{{ component_vars.is_filled.'my slot' }}`.
So as workaround, we instead use these escaped names which are valid identifiers.
So e.g. `my slot` should be escaped as `my_slot`.
"""
# NOTE: Do a simple substitution where we replace all non-identifier characters with `_`.
# Identifiers consist of alphanum (a-zA-Z0-9) and underscores.
# We don't check if these escaped names conflict with other existing slots in the template,
# we leave this obligation to the user.
escaped_name = name_escape_re.sub("_", name)
return escaped_name
class SlotNode(BaseNode):
"""
@ -796,7 +815,7 @@ class SlotNode(BaseNode):
and _COMPONENT_CONTEXT_KEY in component_ctx.outer_context
):
extra_context[_COMPONENT_CONTEXT_KEY] = component_ctx.outer_context[_COMPONENT_CONTEXT_KEY]
# This ensures that `component_vars.is_filled`is accessible in the fill
# This ensures that the ComponentVars API (e.g. `{{ component_vars.is_filled }}`) is accessible in the fill
extra_context["component_vars"] = component_ctx.outer_context["component_vars"]
# Irrespective of which context we use ("root" context or the one passed to this
@ -1419,25 +1438,6 @@ def normalize_slot_fills(
return norm_fills
name_escape_re = re.compile(r"[^\w]")
def _escape_slot_name(name: str) -> str:
"""
Users may define slots with names which are invalid identifiers like 'my slot'.
But these cannot be used as keys in the template context, e.g. `{{ component_vars.is_filled.'my slot' }}`.
So as workaround, we instead use these escaped names which are valid identifiers.
So e.g. `my slot` should be escaped as `my_slot`.
"""
# NOTE: Do a simple substitution where we replace all non-identifier characters with `_`.
# Identifiers consist of alphanum (a-zA-Z0-9) and underscores.
# We don't check if these escaped names conflict with other existing slots in the template,
# we leave this obligation to the user.
escaped_name = name_escape_re.sub("_", name)
return escaped_name
def _nodelist_to_slot(
component_name: str,
slot_name: Optional[str],

View file

@ -24,7 +24,7 @@ class Empty(NamedTuple):
pass
```
Read more about [Typing and validation](../../concepts/advanced/typing_and_validation).
Read more about [Typing and validation](../../concepts/fundamentals/typing_and_validation).
"""
pass

View file

@ -4,7 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
import re
from typing import no_type_check
from typing import NamedTuple
import pytest
from django.conf import settings
@ -19,6 +19,8 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import (
Component,
ComponentView,
Slot,
SlotInput,
all_components,
get_component_by_class_id,
register,
@ -316,7 +318,6 @@ class TestComponentRenderAPI:
def test_input(self):
class TestComponent(Component):
@no_type_check
def get_template_data(self, args, kwargs, slots, context):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
@ -329,7 +330,6 @@ class TestComponentRenderAPI:
"variable": kwargs["variable"],
}
@no_type_check
def get_template(self, context):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
@ -358,6 +358,323 @@ class TestComponentRenderAPI:
""",
)
def test_args_kwargs_slots__simple(self):
called = False
class TestComponent(Component):
template = ""
def get_template_data(self, args, kwargs, slots, context):
nonlocal called
called = True
assert self.args == [123, "str"]
assert self.kwargs == {"variable": "test", "another": 1}
assert list(self.slots.keys()) == ["my_slot"]
my_slot = self.slots["my_slot"]
assert my_slot() == "MY_SLOT"
TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
assert called
def test_args_kwargs_slots__typed(self):
called = False
class TestComponent(Component):
template = ""
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
def get_template_data(self, args, kwargs, slots, context):
nonlocal called
called = True
assert self.args == TestComponent.Args(123, "str")
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1)
assert isinstance(self.slots, TestComponent.Slots)
assert isinstance(self.slots.my_slot, Slot)
assert self.slots.my_slot() == "MY_SLOT"
# Check that the instances are reused across multiple uses
assert self.args is self.args
assert self.kwargs is self.kwargs
assert self.slots is self.slots
TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
assert called
def test_args_kwargs_slots__raises_outside_render(self):
class TestComponent(Component):
template = ""
comp = TestComponent()
with pytest.raises(RuntimeError):
comp.args
with pytest.raises(RuntimeError):
comp.kwargs
with pytest.raises(RuntimeError):
comp.slots
@djc_test
class TestComponentTemplateVars:
def test_args_kwargs_slots__simple_untyped(self):
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.0|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
args: [123, 'str']
kwargs: {'variable': 'test', 'another': 1}
slots: {'my_slot': <Slot component_name='TestComponent' slot_name='my_slot'>}
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
""",
)
def test_args_kwargs_slots__simple_typed(self):
class TestComponent(Component):
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.variable|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
args: Args(variable=123, another='str')
kwargs: Kwargs(variable='test', another=1)
slots: Slots(my_slot=<Slot component_name='TestComponent' slot_name='my_slot'>)
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
""",
)
def test_args_kwargs_slots__nested_untyped(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.0|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc40="">
args: [123, 'str']
kwargs: {'variable': 'test', 'another': 1}
slots: {'my_slot': <Slot component_name='TestComponent' slot_name='my_slot'>}
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
</div>
""",
)
def test_args_kwargs_slots__nested_typed(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
class Args(NamedTuple):
variable: int
another: str
class Kwargs(NamedTuple):
variable: str
another: int
class Slots(NamedTuple):
my_slot: SlotInput
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{# Test whole objects #}
args: {{ component_vars.args|safe }}
kwargs: {{ component_vars.kwargs|safe }}
slots: {{ component_vars.slots|safe }}
{# Test individual values #}
arg: {{ component_vars.args.variable|safe }}
kwarg: {{ component_vars.kwargs.variable|safe }}
slot: {{ component_vars.slots.my_slot|safe }}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
args=[123, "str"],
kwargs={"variable": "test", "another": 1},
slots={"my_slot": "MY_SLOT"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc40="">
args: Args(variable=123, another='str')
kwargs: Kwargs(variable='test', another=1)
slots: Slots(my_slot=<Slot component_name='TestComponent' slot_name='my_slot'>)
arg: 123
kwarg: test
slot: <Slot component_name='TestComponent' slot_name='my_slot'>
</div>
</div>
""",
)
def test_args_kwargs_slots__nested_conditional_slots(self):
@register("wrapper")
class Wrapper(Component):
template: types.django_html = """
{% load component_tags %}
<div class="wrapper">
{% slot "content" default %}
<div class="test">DEFAULT</div>
{% endslot %}
</div>
"""
class TestComponent(Component):
template: types.django_html = """
{% load component_tags %}
<div class="test-component">
{% component "wrapper" %}
{% if component_vars.slots.subtitle %}
<div class="subtitle">
{% slot "subtitle" %}
Optional subtitle
{% endslot %}
</div>
{% endif %}
{% endcomponent %}
</div>
"""
html = TestComponent.render(
slots={"subtitle": "SUBTITLE_FILLED"},
)
assertHTMLEqual(
html,
"""
<div class="test-component" data-djc-id-ca1bc3e="">
<div class="wrapper" data-djc-id-ca1bc41="">
<div class="subtitle">SUBTITLE_FILLED</div>
</div>
</div>
""",
)
@djc_test
class TestComponentRender:

View file

@ -959,6 +959,7 @@ class TestOuterContextProperty:
assert "outer_value" in rendered
# TODO_v1: Remove, superseded by `component_vars.slots`
@djc_test
class TestContextVarsIsFilled:
class IsFilledVarsComponent(Component):