mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 20:29:44 +00:00
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:
parent
d514694788
commit
e054a68715
14 changed files with 1262 additions and 217 deletions
126
CHANGELOG.md
126
CHANGELOG.md
|
@ -4,6 +4,17 @@
|
||||||
|
|
||||||
⚠️ Major release ⚠️ - Please test thoroughly before / after upgrading.
|
⚠️ 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
|
#### 🚨📢 BREAKING CHANGES
|
||||||
|
|
||||||
**Middleware**
|
**Middleware**
|
||||||
|
@ -68,7 +79,7 @@
|
||||||
text: str
|
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.
|
- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type.
|
||||||
|
|
||||||
|
@ -434,6 +445,66 @@
|
||||||
{% endfill %}
|
{% 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
|
#### Feat
|
||||||
|
|
||||||
- New method to render template variables - `get_template_data()`
|
- 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
|
This practically brings back input validation, because the instantiation of the types
|
||||||
will raise an error if the inputs are not valid.
|
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"
|
- 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.
|
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.
|
- `get_component_url()` now optionally accepts `query` and `fragment` arguments.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -237,9 +237,9 @@ class Table(Component):
|
||||||
assert self.id == "djc1A2b3c"
|
assert self.id == "djc1A2b3c"
|
||||||
|
|
||||||
# Access component's inputs and slots
|
# Access component's inputs and slots
|
||||||
assert self.input.args == (123, "str")
|
assert self.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.kwargs == {"variable": "test", "another": 1}
|
||||||
footer_slot = self.input.slots["footer"]
|
footer_slot = self.slots["footer"]
|
||||||
some_var = self.input.context["some_var"]
|
some_var = self.input.context["some_var"]
|
||||||
|
|
||||||
# Access the request object and Django's context processors, if available
|
# Access the request object and Django's context processors, if available
|
||||||
|
@ -390,7 +390,7 @@ class Header(Component):
|
||||||
|
|
||||||
### Input validation and static type hints
|
### 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, and more:
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ and so `selected_items` will be set to `[1, 2, 3]`.
|
||||||
|
|
||||||
!!! warning
|
!!! 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),
|
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||||
you may be inclined to define the defaults in the classes.
|
you may be inclined to define the defaults in the classes.
|
||||||
|
|
|
@ -152,75 +152,141 @@ The difference is that:
|
||||||
|
|
||||||
## Accessing component inputs
|
## 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
|
```python
|
||||||
|
class ProfileCard(Component):
|
||||||
|
# Access inputs directly as parameters
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"user_id": args[0],
|
||||||
|
"show_details": kwargs["show_details"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
By default, the `args` parameter 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 ProfileCard(Component):
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
class Args(NamedTuple):
|
||||||
# Access inputs directly as parameters
|
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 {
|
return {
|
||||||
"user_id": user_id,
|
"user_id": args.user_id,
|
||||||
"show_details": show_details,
|
"show_details": kwargs.show_details,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info
|
### `args`, `kwargs`, `slots` properties
|
||||||
|
|
||||||
By default, the `args` parameter is a list, while `kwargs` and `slots` are dictionaries.
|
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:
|
||||||
|
|
||||||
If you add typing to your component with
|
```py
|
||||||
[`Args`](../../../reference/api/#django_components.Component.Args),
|
class ProfileCard(Component):
|
||||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
def on_render_before(self, *args, **kwargs):
|
||||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
# Access inputs via self.args, self.kwargs, self.slots
|
||||||
the respective inputs will be given as instances of these classes.
|
self.args[0]
|
||||||
|
self.kwargs.get("show_details", False)
|
||||||
|
self.slots["footer"]
|
||||||
|
```
|
||||||
|
|
||||||
Learn more about [Component typing](../../advanced/typing_and_validation).
|
!!! info
|
||||||
|
|
||||||
2. **`self.input` property**
|
These properties work the same way as `args`, `kwargs`, and `slots` parameters in the data methods:
|
||||||
|
|
||||||
The data methods receive only the main inputs. There are additional settings that may be passed
|
By default, the `args` property is a list, while `kwargs` and `slots` are dictionaries.
|
||||||
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)).
|
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.
|
||||||
|
|
||||||
This includes:
|
Learn more about [Component typing](../../fundamentals/typing_and_validation).
|
||||||
|
|
||||||
- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
|
```py
|
||||||
- [`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):
|
class ProfileCard(Component):
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
class Args(NamedTuple):
|
||||||
# Access positional arguments
|
user_id: int
|
||||||
user_id = self.input.args[0] if self.input.args else None
|
|
||||||
|
|
||||||
# Access keyword arguments
|
class Kwargs(NamedTuple):
|
||||||
show_details = self.input.kwargs.get("show_details", False)
|
show_details: bool
|
||||||
|
|
||||||
# Render component differently depending on the type
|
|
||||||
if self.input.type == "fragment":
|
|
||||||
...
|
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
||||||
return {
|
return {
|
||||||
"user_id": user_id,
|
"user_id": self.args.user_id,
|
||||||
"show_details": show_details,
|
"show_details": self.kwargs.show_details,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info
|
### `input` property (low-level)
|
||||||
|
|
||||||
Unlike the parameters passed to the data methods, the `args`, `kwargs`, and `slots` in `self.input` property are always lists and dictionaries,
|
The previous two approaches allow you to access only the most important inputs.
|
||||||
regardless of whether you added typing to your component.
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Access keyword arguments
|
||||||
|
show_details = self.input.kwargs.get("show_details", False)
|
||||||
|
|
||||||
|
# Render component differently depending on the type
|
||||||
|
if self.input.type == "fragment":
|
||||||
|
...
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"show_details": show_details,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! 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 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
|
## 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:
|
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.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.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.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
|
- [`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.
|
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
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
|
|
@ -20,24 +20,21 @@ Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def on_render_before(self, context, template):
|
||||||
# Access component's ID
|
# Access component's ID
|
||||||
assert self.id == "c1A2b3c"
|
assert self.id == "c1A2b3c"
|
||||||
|
|
||||||
# Access component's inputs, slots and context
|
# Access component's inputs, slots and context
|
||||||
assert self.input.args == (123, "str")
|
assert self.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.kwargs == {"variable": "test", "another": 1}
|
||||||
footer_slot = self.input.slots["footer"]
|
footer_slot = self.slots["footer"]
|
||||||
some_var = self.input.context["some_var"]
|
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
|
# Access the request object and Django's context processors, if available
|
||||||
assert self.request.GET == {"query": "something"}
|
assert self.request.GET == {"query": "something"}
|
||||||
assert self.context_processors_data['user'].username == "admin"
|
assert self.context_processors_data['user'].username == "admin"
|
||||||
|
|
||||||
return {
|
|
||||||
"variable": variable,
|
|
||||||
}
|
|
||||||
|
|
||||||
rendered = Table.render(
|
rendered = Table.render(
|
||||||
kwargs={"variable": "test", "another": 1},
|
kwargs={"variable": "test", "another": 1},
|
||||||
args=(123, "str"),
|
args=(123, "str"),
|
||||||
|
@ -49,42 +46,159 @@ rendered = Table.render(
|
||||||
|
|
||||||
The Render API includes:
|
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.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.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.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
|
- [`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)
|
If you defined the [`Component.Args`](../../../reference/api/#django_components.Component.Args) class,
|
||||||
multiple times, the ID will be different for each call.
|
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`,
|
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||||
where `XXXXXX` is a random string of 6 alphanumeric characters (case-sensitive).
|
|
||||||
|
|
||||||
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.
|
With `Args` class:
|
||||||
|
|
||||||
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
|
```python
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
class Args(NamedTuple):
|
||||||
# Access component's ID
|
page: int
|
||||||
assert self.id == "c1A2b3c"
|
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
|
## 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:
|
[`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
|
## Request object and context processors
|
||||||
|
|
||||||
Components have access to the request object and context processors data if the component was:
|
Components have access to the request object and context processors data if the component was:
|
||||||
|
|
|
@ -484,7 +484,7 @@ in component's [`Args`](../../../reference/api/#django_components.Component.Args
|
||||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||||
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes.
|
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
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
|
|
@ -125,8 +125,8 @@ Thus, you can check where a slot was filled from by printing it out:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
def on_render_before(self):
|
def on_render_before(self, *args, **kwargs):
|
||||||
print(self.input.slots)
|
print(self.slots)
|
||||||
```
|
```
|
||||||
|
|
||||||
might print:
|
might print:
|
||||||
|
|
|
@ -227,9 +227,9 @@ class Table(Component):
|
||||||
assert self.id == "djc1A2b3c"
|
assert self.id == "djc1A2b3c"
|
||||||
|
|
||||||
# Access component's inputs and slots
|
# Access component's inputs and slots
|
||||||
assert self.input.args == (123, "str")
|
assert self.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.kwargs == {"variable": "test", "another": 1}
|
||||||
footer_slot = self.input.slots["footer"]
|
footer_slot = self.slots["footer"]
|
||||||
some_var = self.input.context["some_var"]
|
some_var = self.input.context["some_var"]
|
||||||
|
|
||||||
# Access the request object and Django's context processors, if available
|
# Access the request object and Django's context processors, if available
|
||||||
|
@ -380,9 +380,9 @@ class Header(Component):
|
||||||
|
|
||||||
### Input validation and static type hints
|
### 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
|
```py
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
|
|
@ -169,28 +169,43 @@ class ComponentInput:
|
||||||
This object is available only during render under [`Component.input`](../api#django_components.Component.input).
|
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).
|
Read more about the [Render API](../../concepts/fundamentals/render_api).
|
||||||
|
|
||||||
This class can be typed as:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
context: Context
|
context: Context
|
||||||
|
"""
|
||||||
|
Django's [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||||
|
passed to `Component.render()`
|
||||||
|
"""
|
||||||
args: List
|
args: List
|
||||||
|
"""Positional arguments (as list) passed to `Component.render()`"""
|
||||||
kwargs: Dict
|
kwargs: Dict
|
||||||
|
"""Keyword arguments (as dict) passed to `Component.render()`"""
|
||||||
slots: Dict[SlotName, Slot]
|
slots: Dict[SlotName, Slot]
|
||||||
|
"""Slots (as dict) passed to `Component.render()`"""
|
||||||
deps_strategy: DependenciesStrategy
|
deps_strategy: DependenciesStrategy
|
||||||
|
"""Dependencies strategy passed to `Component.render()`"""
|
||||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||||
type: DependenciesStrategy
|
type: DependenciesStrategy
|
||||||
"""Deprecated alias for `deps_strategy`."""
|
"""Deprecated. Will be removed in v1. Use `deps_strategy` instead."""
|
||||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||||
render_dependencies: bool
|
render_dependencies: bool
|
||||||
"""Deprecated. Instead use `deps_strategy="ignore"`."""
|
"""Deprecated. Will be removed in v1. Use `deps_strategy="ignore"` instead."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass()
|
@dataclass()
|
||||||
class MetadataItem:
|
class MetadataItem:
|
||||||
render_id: str
|
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
|
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]
|
request: Optional[HttpRequest]
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,15 +214,174 @@ class ComponentVars(NamedTuple):
|
||||||
Type for the variables available inside the component templates.
|
Type for the variables available inside the component templates.
|
||||||
|
|
||||||
All variables here are scoped under `component_vars.`, so e.g. attribute
|
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
|
```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]
|
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`).
|
Dictonary describing which component slots are filled (`True`) or are not (`False`).
|
||||||
|
|
||||||
<i>New in version 0.70</i>
|
<i>New in version 0.70</i>
|
||||||
|
@ -234,7 +408,7 @@ class ComponentVars(NamedTuple):
|
||||||
"my_slot_filled": "my_slot" in slots
|
"my_slot_filled": "my_slot" in slots
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
# Descriptor to pass getting/setting of `template_name` onto `template_file`
|
# 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)
|
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)
|
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
|
!!! 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`
|
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.
|
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:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -786,7 +960,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
- Set type hints for this data.
|
- Set type hints for this data.
|
||||||
- Document the component 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
|
!!! 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`
|
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.
|
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:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -1049,7 +1223,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
- Set type hints for this data.
|
- Set type hints for this data.
|
||||||
- Document the component 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
|
!!! 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`
|
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.
|
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:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -1319,7 +1493,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
- Set type hints for this data.
|
- Set type hints for this data.
|
||||||
- Document the component 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
|
!!! info
|
||||||
|
|
||||||
|
@ -1703,14 +1877,14 @@ class Component(metaclass=ComponentMeta):
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
# Access component's inputs, slots and context
|
# Access component's inputs, slots and context
|
||||||
assert self.input.args == [123, "str"]
|
assert self.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.kwargs == {"variable": "test", "another": 1}
|
||||||
footer_slot = self.input.slots["footer"]
|
footer_slot = self.slots["footer"]
|
||||||
some_var = self.input.context["some_var"]
|
some_var = self.input.context["some_var"]
|
||||||
|
|
||||||
rendered = TestComponent.render(
|
rendered = TestComponent.render(
|
||||||
kwargs={"variable": "test", "another": 1},
|
kwargs={"variable": "test", "another": 1},
|
||||||
args=(123, "str"),
|
args=[123, "str"],
|
||||||
slots={"footer": "MY_SLOT"},
|
slots={"footer": "MY_SLOT"},
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -1723,15 +1897,196 @@ class Component(metaclass=ComponentMeta):
|
||||||
return self._metadata_stack[-1].input
|
return self._metadata_stack[-1].input
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_filled(self) -> SlotIsFilled:
|
def args(self) -> Any:
|
||||||
"""
|
"""
|
||||||
Dictionary describing which slots have or have not been filled.
|
The `args` argument as passed to
|
||||||
|
[`Component.get_template_data()`](../api/#django_components.Component.get_template_data).
|
||||||
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.
|
|
||||||
|
|
||||||
Raises `RuntimeError` if accessed outside of rendering execution.
|
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):
|
if not len(self._metadata_stack):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{self.name}: Tried to access Component's `is_filled` attribute "
|
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),
|
[`Kwargs`](../api/#django_components.Component.Kwargs),
|
||||||
and [`Slots`](../api/#django_components.Component.Slots) classes.
|
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
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
@ -2314,6 +2669,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
deps_strategy: DependenciesStrategy = "document",
|
deps_strategy: DependenciesStrategy = "document",
|
||||||
request: Optional[HttpRequest] = None,
|
request: Optional[HttpRequest] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
######################################
|
||||||
|
# 1. Handle inputs
|
||||||
|
######################################
|
||||||
|
|
||||||
# Allow to pass down Request object via context.
|
# Allow to pass down Request object via context.
|
||||||
# `context` may be passed explicitly via `Component.render()` and `Component.render_to_response()`,
|
# `context` may be passed explicitly via `Component.render()` and `Component.render_to_response()`,
|
||||||
# or implicitly via `{% component %}` tag.
|
# or implicitly via `{% component %}` tag.
|
||||||
|
@ -2335,6 +2694,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
slots_dict = normalize_slot_fills(
|
slots_dict = normalize_slot_fills(
|
||||||
to_dict(slots) if slots is not None else {},
|
to_dict(slots) if slots is not None else {},
|
||||||
escape_slots_content,
|
escape_slots_content,
|
||||||
|
component_name=self.name,
|
||||||
)
|
)
|
||||||
# Use RequestContext if request is provided, so that child non-component template tags
|
# Use RequestContext if request is provided, so that child non-component template tags
|
||||||
# can access the request object too.
|
# can access the request object too.
|
||||||
|
@ -2346,34 +2706,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
if not isinstance(context, Context):
|
if not isinstance(context, Context):
|
||||||
context = RequestContext(request, context) if request else 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()
|
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
|
# Allow plugins to modify or validate the inputs
|
||||||
result_override = extensions.on_component_input(
|
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 component rendering was short-circuited by an extension, skipping
|
||||||
# the rest of the rendering process. This may be for example a cached content.
|
# the rest of the rendering process. This may be for example a cached content.
|
||||||
if result_override is not None:
|
if result_override is not None:
|
||||||
# Cleanup needs to be done even if short-circuited
|
|
||||||
context.render_context.pop()
|
|
||||||
return result_override
|
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.
|
# We pass down the components the info about the component's parent.
|
||||||
# This is used for correctly resolving slot fills, correct rendering order,
|
# This is used for correctly resolving slot fills, correct rendering order,
|
||||||
# or CSS scoping.
|
# 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
|
# 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`.
|
# 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
|
# This way, the flow is easier to debug. Because otherwise, if you tried to print out
|
||||||
# or inspect the Context object, your screen is filled with the deeply nested ComponentContext objects.
|
# 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
|
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
|
# Allow to access component input and metadata like component ID from within data methods
|
||||||
with self._with_metadata(metadata):
|
with self._with_metadata(metadata):
|
||||||
template_data, js_data, css_data = self._call_data_methods(args_list, kwargs_dict, slots_dict, context)
|
template_data, js_data, css_data = self._call_data_methods(
|
||||||
# Prepare context processors data, so we don't have to call `_with_metadata() later again
|
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
|
context_processors_data = self.context_processors_data
|
||||||
|
|
||||||
extensions.on_component_data(
|
extensions.on_component_data(
|
||||||
|
@ -2478,15 +2862,17 @@ class Component(metaclass=ComponentMeta):
|
||||||
cache_component_css(self.__class__)
|
cache_component_css(self.__class__)
|
||||||
css_input_hash = cache_component_css_vars(self.__class__, css_data) if css_data else None
|
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:
|
with _prepare_template(self, context, template_data, metadata) as template:
|
||||||
component_ctx.template_name = template.name
|
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(
|
with context.update(
|
||||||
{
|
{
|
||||||
# Make data from context processors available inside templates
|
# 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
|
# NOTE: Public API for variables accessible from within a component's template
|
||||||
# See https://github.com/django-components/django-components/issues/280#issuecomment-2081180940
|
# See https://github.com/django-components/django-components/issues/280#issuecomment-2081180940
|
||||||
"component_vars": ComponentVars(
|
"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
|
# Cleanup
|
||||||
context.render_context.pop()
|
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
|
# 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.
|
# in the template, we defer rendering in order to scalably handle deeply nested components.
|
||||||
#
|
#
|
||||||
|
@ -2662,19 +3064,22 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
return renderer
|
return renderer
|
||||||
|
|
||||||
def _call_data_methods(self, args: Any, kwargs: Any, slots: Any, context: Context) -> Tuple[Dict, Dict, Dict]:
|
def _call_data_methods(
|
||||||
# If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain
|
self,
|
||||||
# dicts / lists.
|
args: Any,
|
||||||
args_inst = self.Args(*args) if self.Args is not None else args
|
kwargs: Any,
|
||||||
kwargs_inst = self.Kwargs(**kwargs) if self.Kwargs is not None else kwargs
|
slots: Any,
|
||||||
slots_inst = self.Slots(**slots) if self.Slots is not None else slots
|
context: Context,
|
||||||
|
# TODO_V2 - Remove `raw_args` and `raw_kwargs` in v2
|
||||||
|
raw_args: List,
|
||||||
|
raw_kwargs: Dict,
|
||||||
|
) -> Tuple[Dict, Dict, Dict]:
|
||||||
# Template data
|
# 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, {}))
|
new_template_data = to_dict(default(maybe_template_data, {}))
|
||||||
|
|
||||||
# TODO_V2 - Remove this in v2
|
# 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:
|
if legacy_template_data and new_template_data:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Component {self.name} has both `get_context_data()` and `get_template_data()` methods. "
|
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
|
# TODO - Enable JS and CSS vars - expose, and document
|
||||||
# JS data
|
# 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, {}))
|
js_data = to_dict(default(maybe_js_data, {}))
|
||||||
|
|
||||||
# CSS 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, {}))
|
css_data = to_dict(default(maybe_css_data, {}))
|
||||||
|
|
||||||
# Validate outputs
|
# Validate outputs
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import inspect
|
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
|
from django.template import Context, Template
|
||||||
|
|
||||||
|
@ -99,35 +99,24 @@ class DynamicComponent(Component):
|
||||||
|
|
||||||
_is_dynamic_component = True
|
_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()`
|
# 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
|
# 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.
|
# 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
|
# E.g. the `_COMPONENT_CONTEXT_KEY` is set, which means that the child component
|
||||||
# will know that it's a child of this component.
|
# will know that it's a child of this component.
|
||||||
def on_render_before(self, context: Context, template: Template) -> Context:
|
def on_render_before(self, context: Context, template: Template) -> Context:
|
||||||
comp_class: type[Component] = context["comp_class"]
|
# Make a copy of kwargs so we pass to the child only the kwargs that are
|
||||||
args = context["args"]
|
# actually used by the child component.
|
||||||
kwargs = context["kwargs"]
|
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(
|
comp = comp_class(
|
||||||
registered_name=self.registered_name,
|
registered_name=self.registered_name,
|
||||||
|
@ -136,8 +125,8 @@ class DynamicComponent(Component):
|
||||||
)
|
)
|
||||||
output = comp.render(
|
output = comp.render(
|
||||||
context=self.input.context,
|
context=self.input.context,
|
||||||
args=args,
|
args=self.input.args,
|
||||||
kwargs=kwargs,
|
kwargs=cleared_kwargs,
|
||||||
slots=self.input.slots,
|
slots=self.input.slots,
|
||||||
# NOTE: Since we're accessing slots as `self.input.slots`, the content of slot functions
|
# NOTE: Since we're accessing slots as `self.input.slots`, the content of slot functions
|
||||||
# was already escaped (if set so).
|
# was already escaped (if set so).
|
||||||
|
@ -145,6 +134,7 @@ class DynamicComponent(Component):
|
||||||
deps_strategy=self.input.deps_strategy,
|
deps_strategy=self.input.deps_strategy,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set the output to the context so it can be accessed from within the template.
|
||||||
context["output"] = output
|
context["output"] = output
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -401,18 +401,37 @@ class SlotFallback:
|
||||||
SlotRef = SlotFallback
|
SlotRef = SlotFallback
|
||||||
|
|
||||||
|
|
||||||
|
name_escape_re = re.compile(r"[^\w]")
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_v1 - Remove, superseded by `Component.slots` and `component_vars.slots`
|
||||||
class SlotIsFilled(dict):
|
class SlotIsFilled(dict):
|
||||||
"""
|
"""
|
||||||
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
|
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, fills: Dict, *args: Any, **kwargs: Any) -> None:
|
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)
|
super().__init__(escaped_fill_names, *args, **kwargs)
|
||||||
|
|
||||||
def __missing__(self, key: Any) -> bool:
|
def __missing__(self, key: Any) -> bool:
|
||||||
return False
|
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):
|
class SlotNode(BaseNode):
|
||||||
"""
|
"""
|
||||||
|
@ -796,7 +815,7 @@ class SlotNode(BaseNode):
|
||||||
and _COMPONENT_CONTEXT_KEY in component_ctx.outer_context
|
and _COMPONENT_CONTEXT_KEY in component_ctx.outer_context
|
||||||
):
|
):
|
||||||
extra_context[_COMPONENT_CONTEXT_KEY] = component_ctx.outer_context[_COMPONENT_CONTEXT_KEY]
|
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"]
|
extra_context["component_vars"] = component_ctx.outer_context["component_vars"]
|
||||||
|
|
||||||
# Irrespective of which context we use ("root" context or the one passed to this
|
# 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
|
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(
|
def _nodelist_to_slot(
|
||||||
component_name: str,
|
component_name: str,
|
||||||
slot_name: Optional[str],
|
slot_name: Optional[str],
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Empty(NamedTuple):
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
Read more about [Typing and validation](../../concepts/advanced/typing_and_validation).
|
Read more about [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -4,7 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import no_type_check
|
from typing import NamedTuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -19,6 +19,8 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||||
from django_components import (
|
from django_components import (
|
||||||
Component,
|
Component,
|
||||||
ComponentView,
|
ComponentView,
|
||||||
|
Slot,
|
||||||
|
SlotInput,
|
||||||
all_components,
|
all_components,
|
||||||
get_component_by_class_id,
|
get_component_by_class_id,
|
||||||
register,
|
register,
|
||||||
|
@ -316,7 +318,6 @@ class TestComponentRenderAPI:
|
||||||
|
|
||||||
def test_input(self):
|
def test_input(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
@no_type_check
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
assert self.input.args == [123, "str"]
|
assert self.input.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||||
|
@ -329,7 +330,6 @@ class TestComponentRenderAPI:
|
||||||
"variable": kwargs["variable"],
|
"variable": kwargs["variable"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@no_type_check
|
|
||||||
def get_template(self, context):
|
def get_template(self, context):
|
||||||
assert self.input.args == [123, "str"]
|
assert self.input.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
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
|
@djc_test
|
||||||
class TestComponentRender:
|
class TestComponentRender:
|
||||||
|
|
|
@ -959,6 +959,7 @@ class TestOuterContextProperty:
|
||||||
assert "outer_value" in rendered
|
assert "outer_value" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_v1: Remove, superseded by `component_vars.slots`
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestContextVarsIsFilled:
|
class TestContextVarsIsFilled:
|
||||||
class IsFilledVarsComponent(Component):
|
class IsFilledVarsComponent(Component):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue