refecator: move defaults applying back to ext, raise on passing Slot to Slot, and docs cleanup (#1214)

* refecator: move defaults applying back to ext, raise on passing Slot to Slot, and docs cleanup

* docs: fix typo
This commit is contained in:
Juro Oravec 2025-05-26 11:59:17 +02:00 committed by GitHub
parent bae0f28813
commit 55b1c8bc62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 317 additions and 147 deletions

View file

@ -12,8 +12,10 @@ Summary:
`get_context_data()` is now deprecated but will remain until v2.
- Slots API polished and prepared for v1.
- Merged `Component.Url` with `Component.View`
- Added `Component.args`, `Component.kwargs`, `Component.slots`
- Added `Component.args`, `Component.kwargs`, `Component.slots`, `Component.context`
- Added `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}`
- You should no longer instantiate `Component` instances. Instead, call `Component.render()` or `Component.render_to_response()` directly.
- Component caching can now consider slots (opt-in)
- And lot more...
#### 🚨📢 BREAKING CHANGES
@ -867,6 +869,8 @@ Summary:
can now be accessed also outside of the render call. So now its possible to take the component
instance out of `get_template_data()` (although this is not recommended).
- Passing `Slot` instance to `Slot` constructor raises an error.
#### Fix
- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).

View file

@ -640,15 +640,25 @@ Table.render(
)
```
Slot class can be instantiated with a function, a string, or from another
[`Slot`](../../../reference/api#django_components.Slot) instance:
Slot class can be instantiated with a function or a string:
```py
slot1 = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
slot2 = Slot("Hello, world!")
slot3 = Slot(slot1)
```
!!! warning
Passing a [`Slot`](../../../reference/api#django_components.Slot) instance to the `Slot`
constructor results in an error:
```py
slot = Slot("Hello")
# Raises an error
slot2 = Slot(slot)
```
### Rendering slots
**Python**
@ -713,12 +723,13 @@ html = slot()
When accessing slots from within [`Component`](../../../reference/api#django_components.Component) methods,
the [`Slot`](../../../reference/api#django_components.Slot) instances are populated
with extra metadata [`component_name`](../../../reference/api#django_components.Slot.component_name)
and [`slot_name`](../../../reference/api#django_components.Slot.slot_name).
with extra metadata [`component_name`](../../../reference/api#django_components.Slot.component_name),
[`slot_name`](../../../reference/api#django_components.Slot.slot_name), and
[`nodelist`](../../../reference/api#django_components.Slot.nodelist).
These are used solely for debugging.
These are used for debugging, such as printing the path to the slot in the component tree.
In fact, you can set these fields too when creating new slots:
When you create a slot, you can set these fields too:
```py
# Either at slot creation
@ -753,19 +764,6 @@ slot = Slot("Hello!")
print(slot.nodelist) # <django.template.Nodelist: ['Hello!']>
```
!!! info
If you pass a [`Slot`](../../../reference/api#django_components.Slot) instance to the constructor,
the inner slot will be "unwrapped" and its `Slot.contents` will be used instead.
```py
slot = Slot("Hello")
print(slot.contents) # "Hello"
slot2 = Slot(slot)
print(slot2.contents) # "Hello"
```
### Escaping slots content
Slots content are automatically escaped by default to prevent XSS attacks.

View file

@ -87,6 +87,14 @@
options:
show_if_no_docstring: true
::: django_components.SlotContext
options:
show_if_no_docstring: true
::: django_components.SlotFallback
options:
show_if_no_docstring: true
::: django_components.SlotFunc
options:
show_if_no_docstring: true

View file

@ -2,7 +2,7 @@
# Commands
These are all the [Django management commands](https://docs.djangoproject.com/en/5.1/ref/django-admin)
These are all the [Django management commands](https://docs.djangoproject.com/en/5.2/ref/django-admin)
that will be added by installing `django_components`:
@ -54,9 +54,7 @@ python manage.py components ext run <extension> <command>
## `components create`
```txt
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
[--dry-run]
name
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
```
@ -238,7 +236,7 @@ List all extensions.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine- readable output.
- Only show table data, without headers. Use this option for generating machine-readable output.
@ -386,7 +384,7 @@ usage: python manage.py components list [-h] [--all] [--columns COLUMNS] [-s]
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/list.py#L141" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/list.py#L89" target="_blank">See source code</a>
@ -401,7 +399,7 @@ List all components created in this project.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine- readable output.
- Only show table data, without headers. Use this option for generating machine-readable output.
@ -463,9 +461,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
## `upgradecomponent`
```txt
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
[--force-color] [--skip-checks]
```
@ -509,10 +506,8 @@ Deprecated. Use `components upgrade` instead.
## `startcomponent`
```txt
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}]
[--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
name
```

View file

@ -85,9 +85,9 @@ name | type | description
`component` | [`Component`](../api#django_components.Component) | The Component instance that received the input and is being rendered
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class
`component_id` | `str` | The unique identifier for this component instance
`context` | [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) | The Django template Context object
`context` | [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) | The Django template Context object
`kwargs` | `Dict` | Dictionary of keyword arguments passed to the component
`slots` | `Dict` | Dictionary of slot definitions
`slots` | `Dict[str, Slot]` | Dictionary of slot definitions
::: django_components.extension.ComponentExtension.on_component_registered
options:
@ -181,6 +181,30 @@ name | type | description
--|--|--
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The to-be-deleted ComponentRegistry instance
::: django_components.extension.ComponentExtension.on_slot_rendered
options:
heading_level: 3
show_root_heading: true
show_signature: true
separate_signature: true
show_symbol_type_heading: false
show_symbol_type_toc: false
show_if_no_docstring: true
show_labels: false
**Available data:**
name | type | description
--|--|--
`component` | [`Component`](../api#django_components.Component) | The Component instance that contains the `{% slot %}` tag
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class that contains the `{% slot %}` tag
`component_id` | `str` | The unique identifier for this component instance
`result` | `SlotResult` | The rendered result of the slot
`slot` | `Slot` | The Slot instance that was rendered
`slot_is_default` | `bool` | Whether the slot is default
`slot_is_required` | `bool` | Whether the slot is required
`slot_name` | `str` | The name of the `{% slot %}` tag
## Objects
::: django_components.extension.OnComponentClassCreatedContext

View file

@ -6,7 +6,7 @@ Below are the signals that are sent by or during the use of django-components.
## template_rendered
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.1/ref/signals/#template-rendered) signal.
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.2/ref/signals/#template-rendered) signal.
This signal is sent when a template is rendered.
Django-components triggers this signal when a component is rendered. If there are nested components,

View file

@ -20,7 +20,7 @@ Import as
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1024" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1022" target="_blank">See source code</a>
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1046" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1044" target="_blank">See source code</a>
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2784" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3164" target="_blank">See source code</a>
@ -120,7 +120,7 @@ and other tags:
### Isolating components
By default, components behave similarly to Django's
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
and the template inside the component has access to the variables defined in the outer template.
You can selectively isolate a component, using the `only` flag, so that the inner template
@ -163,34 +163,32 @@ COMPONENTS = {
## fill
```django
{% fill name: str, *, data: Optional[str] = None, default: Optional[str] = None %}
{% fill name: str, *, data: Optional[str] = None, fallback: Optional[str] = None, body: Union[str, django.utils.safestring.SafeString, django_components.slots.SlotFunc[~TSlotData], django_components.slots.Slot[~TSlotData], NoneType] = None, default: Optional[str] = None %}
{% endfill %}
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L648" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L949" target="_blank">See source code</a>
Use this tag to insert content into component's slots.
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block.
Runtime checks should prohibit other usages.
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block,
and raises a `TemplateSyntaxError` if used outside of a component.
**Args:**
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
the default slot.
- `default` (str, optional): This argument allows you to access the original content of the slot
under the specified variable name. See
[Accessing original content of slots](../../concepts/fundamentals/slots#accessing-original-content-of-slots)
- `data` (str, optional): This argument allows you to access the data passed to the slot
under the specified variable name. See [Scoped slots](../../concepts/fundamentals/slots#scoped-slots)
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
- `fallback` (str, optional): This argument allows you to access the original content of the slot
under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
**Examples:**
**Example:**
Basic usage:
```django
{% component "my_table" %}
{% fill "pagination" %}
@ -199,7 +197,15 @@ Basic usage:
{% endcomponent %}
```
### Accessing slot's default content with the `default` kwarg
### Access slot fallback
Use the `fallback` kwarg to access the original content of the slot.
The `fallback` kwarg defines the name of the variable that will contain the slot's fallback content.
Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
Component template:
```django
{# my_table.html #}
@ -211,17 +217,27 @@ Basic usage:
</table>
```
Fill:
```django
{% component "my_table" %}
{% fill "pagination" default="default_pag" %}
{% fill "pagination" fallback="fallback" %}
<div class="my-class">
{{ default_pag }}
{{ fallback }}
</div>
{% endfill %}
{% endcomponent %}
```
### Accessing slot's data with the `data` kwarg
### Access slot data
Use the `data` kwarg to access the data passed to the slot.
The `data` kwarg defines the name of the variable that will contain the slot's data.
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
Component template:
```django
{# my_table.html #}
@ -233,6 +249,8 @@ Basic usage:
</table>
```
Fill:
```django
{% component "my_table" %}
{% fill "pagination" data="slot_data" %}
@ -245,20 +263,60 @@ Basic usage:
{% endcomponent %}
```
### Accessing slot data and default content on the default slot
### Using default slot
To access slot data and the default slot content on the default slot,
To access slot data and the fallback slot content on the default slot,
use `{% fill %}` with `name` set to `"default"`:
```django
{% component "button" %}
{% fill name="default" data="slot_data" default="default_slot" %}
{% fill name="default" data="slot_data" fallback="slot_fallback" %}
You clicked me {{ slot_data.count }} times!
{{ default_slot }}
{{ slot_fallback }}
{% endfill %}
{% endcomponent %}
```
### Slot fills from Python
You can pass a slot fill from Python to a component by setting the `body` kwarg
on the `{% fill %}` tag.
First pass a [`Slot`](../api#django_components.Slot) instance to the template
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
method:
```python
from django_components import component, Slot
class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
return {
"my_slot": Slot(lambda ctx: "Hello, world!"),
}
```
Then pass the slot to the `{% fill %}` tag:
```django
{% component "table" %}
{% fill "pagination" body=my_slot / %}
{% endcomponent %}
```
!!! warning
If you define both the `body` kwarg and the `{% fill %}` tag's body,
an error will be raised.
```django
{% component "table" %}
{% fill "pagination" body=my_slot %}
...
{% endfill %}
{% endcomponent %}
```
## html_attrs
```django
@ -274,8 +332,7 @@ use `{% fill %}` with `name` set to `"default"`:
Generate HTML attributes (`key="value"`), combining data from multiple sources,
whether its template variables or static text.
It is designed to easily merge HTML attributes passed from outside with the internal.
See how to in [Passing HTML attributes to components](../../guides/howto/passing_html_attrs/).
It is designed to easily merge HTML attributes passed from outside as well as inside the component.
**Args:**
@ -318,8 +375,8 @@ renders
<div class="my-class extra-class" data-id="123">
```
**See more usage examples in
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).**
See more usage examples in
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).
## provide
@ -352,7 +409,7 @@ or Vue's [`provide()`](https://vuejs.org/guide/components/provide-inject).
Provide the "user_data" in parent component:
```python
```djc_py
@register("parent")
class Parent(Component):
template = """
@ -372,7 +429,7 @@ class Parent(Component):
Since the "child" component is used within the `{% provide %} / {% endprovide %}` tags,
we can request the "user_data" using `Component.inject("user_data")`:
```python
```djc_py
@register("child")
class Child(Component):
template = """
@ -410,7 +467,7 @@ user = self.inject("user_data")["user"]
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L189" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L475" target="_blank">See source code</a>
@ -435,7 +492,7 @@ or [React's `children`](https://react.dev/learn/passing-props-to-a-component#pas
**Example:**
```python
```djc_py
@register("child")
class Child(Component):
template = """
@ -450,7 +507,7 @@ class Child(Component):
"""
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = """
@ -468,12 +525,14 @@ class Parent(Component):
"""
```
### Passing data to slots
### Slot data
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
tag via fill's `data` kwarg:
```python
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
```djc_py
@register("child")
class Child(Component):
template = """
@ -486,7 +545,7 @@ class Child(Component):
"""
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = """
@ -501,35 +560,35 @@ class Parent(Component):
"""
```
### Accessing default slot content
### Slot fallback
The content between the `{% slot %}..{% endslot %}` tags is the default content that
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
will be rendered if no fill is given for the slot.
This default content can then be accessed from within the [`{% fill %}`](#fill) tag using
the fill's `default` kwarg.
This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
```python
```djc_py
@register("child")
class Child(Component):
template = """
<div>
{% slot "content" %}
This is default content!
This is fallback content!
{% endslot %}
</div>
"""
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = """
{# Parent can access the slot's default content #}
{# Parent can access the slot's fallback content #}
{% component "child" %}
{% fill "content" default="default" %}
{{ default }}
{% fill "content" fallback="fallback" %}
{{ fallback }}
{% endfill %}
{% endcomponent %}
"""

View file

@ -7,5 +7,11 @@ template and in [`on_render_before` / `on_render_after`](../concepts/advanced/ho
hooks.
::: django_components.component.ComponentVars.args
::: django_components.component.ComponentVars.kwargs
::: django_components.component.ComponentVars.slots
::: django_components.component.ComponentVars.is_filled

View file

@ -56,7 +56,7 @@ from django_components.extension import (
extensions,
)
from django_components.extensions.cache import ComponentCache
from django_components.extensions.defaults import ComponentDefaults, apply_defaults, defaults_by_component
from django_components.extensions.defaults import ComponentDefaults
from django_components.extensions.view import ComponentView, ViewFn
from django_components.node import BaseNode
from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render
@ -2798,22 +2798,11 @@ class Component(metaclass=ComponentMeta):
render_id = _gen_component_id()
# Apply defaults to missing or `None` values in `kwargs`
defaults = defaults_by_component.get(comp_cls, None)
if defaults is not None:
apply_defaults(kwargs_dict, defaults)
# If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain
# dicts / lists.
args_inst = comp_cls.Args(*args_list) if comp_cls.Args is not None else args_list
kwargs_inst = comp_cls.Kwargs(**kwargs_dict) if comp_cls.Kwargs is not None else kwargs_dict
slots_inst = comp_cls.Slots(**slots_dict) if comp_cls.Slots is not None else slots_dict
component = comp_cls(
id=render_id,
args=args_inst,
kwargs=kwargs_inst,
slots=slots_inst,
args=args_list,
kwargs=kwargs_dict,
slots=slots_dict,
context=context,
request=request,
deps_strategy=deps_strategy,
@ -2841,6 +2830,12 @@ class Component(metaclass=ComponentMeta):
if result_override is not None:
return result_override
# If user doesn't specify `Args`, `Kwargs`, `Slots` types, then we pass them in as plain
# dicts / lists.
component.args = comp_cls.Args(*args_list) if comp_cls.Args is not None else args_list
component.kwargs = comp_cls.Kwargs(**kwargs_dict) if comp_cls.Kwargs is not None else kwargs_dict
component.slots = comp_cls.Slots(**slots_dict) if comp_cls.Slots is not None else slots_dict
######################################
# 2. Prepare component state
######################################

View file

@ -92,7 +92,7 @@ class OnComponentInputContext(NamedTuple):
"""List of positional arguments passed to the component"""
kwargs: Dict
"""Dictionary of keyword arguments passed to the component"""
slots: Dict
slots: Dict[str, "Slot"]
"""Dictionary of slot definitions"""
context: Context
"""The Django template Context object"""
@ -498,6 +498,19 @@ class ComponentExtension:
# Add extra kwarg to all components when they are rendered
ctx.kwargs["my_input"] = "my_value"
```
!!! warning
In this hook, the components' inputs are still mutable.
As such, if a component defines [`Args`](../api#django_components.Component.Args),
[`Kwargs`](../api#django_components.Component.Kwargs),
[`Slots`](../api#django_components.Component.Slots) types, these types are NOT yet instantiated.
Instead, component fields like [`Component.args`](../api#django_components.Component.args),
[`Component.kwargs`](../api#django_components.Component.kwargs),
[`Component.slots`](../api#django_components.Component.slots)
are plain `list` / `dict` objects.
"""
pass

View file

@ -3,7 +3,7 @@ from dataclasses import MISSING, Field, dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type
from weakref import WeakKeyDictionary
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext, OnComponentInputContext
if TYPE_CHECKING:
from django_components.component import Component
@ -99,7 +99,7 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
return defaults_fields
def apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
"""
Apply the defaults from `Component.Defaults` to the given `kwargs`.
@ -171,3 +171,11 @@ class DefaultsExtension(ComponentExtension):
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
defaults_cls = getattr(ctx.component_cls, "Defaults", None)
defaults_by_component[ctx.component_cls] = _extract_defaults(defaults_cls)
# Apply defaults to missing or `None` values in `kwargs`
def on_component_input(self, ctx: OnComponentInputContext) -> None:
defaults = defaults_by_component.get(ctx.component_cls, None)
if defaults is None:
return
_apply_defaults(ctx.kwargs, defaults)

View file

@ -30,7 +30,7 @@ class ProvideNode(BaseNode):
Provide the "user_data" in parent component:
```python
```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@ -50,7 +50,7 @@ class ProvideNode(BaseNode):
Since the "child" component is used within the `{% provide %} / {% endprovide %}` tags,
we can request the "user_data" using `Component.inject("user_data")`:
```python
```djc_py
@register("child")
class Child(Component):
template = \"\"\"

View file

@ -52,7 +52,23 @@ FILL_BODY_KWARG = "body"
# Public types
SlotResult = Union[str, SafeString]
"""Type representing the result of a slot render function."""
"""
Type representing the result of a slot render function.
**Example:**
```python
from django_components import SlotContext, SlotResult
def my_slot_fn(ctx: SlotContext) -> SlotResult:
return "Hello, world!"
my_slot = Slot(my_slot_fn)
html = my_slot() # Output: Hello, world!
```
Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions).
"""
@dataclass(frozen=True)
@ -65,9 +81,9 @@ class SlotContext(Generic[TSlotData]):
**Example:**
```python
from django_components import SlotContext
from django_components import SlotContext, SlotResult
def my_slot(ctx: SlotContext):
def my_slot(ctx: SlotContext) -> SlotResult:
return f"Hello, {ctx.data['name']}!"
```
@ -216,8 +232,6 @@ class Slot(Generic[TSlotData]):
the body (string) of that `{% fill %}` tag.
- If Slot was created from string as `Slot("...")`, `Slot.contents` will contain that string.
- If Slot was created from a function, `Slot.contents` will contain that function.
- If Slot was created from another `Slot` as `Slot(slot)`, `Slot.contents` will contain the inner
slot's `Slot.contents`.
Read more about [Slot contents](../../concepts/fundamentals/slots#slot-contents).
"""
@ -232,28 +246,30 @@ class Slot(Generic[TSlotData]):
# Following fields are only for debugging
component_name: Optional[str] = None
"""Name of the component that originally received this slot fill."""
"""
Name of the component that originally received this slot fill.
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
"""
slot_name: Optional[str] = None
"""Slot name to which this Slot was initially assigned."""
"""
Slot name to which this Slot was initially assigned.
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
"""
nodelist: Optional[NodeList] = None
"""
If the slot was defined with [`{% fill %}`](../template_tags#fill) tag,
this will be the Nodelist of the fill's content.
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
"""
def __post_init__(self) -> None:
# Since the `Slot` instance is treated as a function, it may be passed as `contents`
# to the `Slot()` constructor. In that case we need to unwrap to the original value
# if `Slot()` constructor got another Slot instance.
# NOTE: If `Slot` was passed as `contents`, we do NOT take the metadata from the inner Slot instance.
# Instead we treat is simply as a function.
# NOTE: Try to avoid infinite loop if `Slot.contents` points to itself.
seen_contents = set()
while isinstance(self.contents, Slot) and self.contents not in seen_contents:
seen_contents.add(id(self.contents))
self.contents = self.contents.contents
if id(self.contents) in seen_contents:
raise ValueError("Detected infinite loop in `Slot.contents` pointing to itself")
# Raise if Slot received another Slot instance as `contents`,
# because this leads to ambiguity about how to handle the metadata.
if isinstance(self.contents, Slot):
raise ValueError("Slot received another Slot instance as `contents`")
if self.content_func is None:
self.contents, new_nodelist, self.content_func = self._resolve_contents(self.contents)
@ -261,7 +277,7 @@ class Slot(Generic[TSlotData]):
self.nodelist = new_nodelist
if not callable(self.content_func):
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
raise ValueError(f"Slot 'content_func' must be a callable, got: {self.content_func}")
# Allow to treat the instances as functions
def __call__(
@ -374,20 +390,30 @@ SlotName = str
class SlotFallback:
"""
SlotFallback allows to treat a slot fallback as a variable. The slot is rendered only once
the instance is coerced to string.
The content between the `{% slot %}..{% endslot %}` tags is the *fallback* content that
will be rendered if no fill is given for the slot.
This is used to access slots as variables inside the templates. When a `SlotFallback`
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
of the slot.
```django
{% slot "name" %}
Hello, my name is {{ name }} <!-- Fallback content -->
{% endslot %}
```
Usage in slot functions:
Because the fallback is defined as a piece of the template
([`NodeList`](https://github.com/django/django/blob/ddb85294159185c5bd5cae34c9ef735ff8409bfe/django/template/base.py#L1017)),
we want to lazily render it only when needed.
`SlotFallback` type allows to pass around the slot fallback as a variable.
To force the fallback to render, coerce it to string to trigger the `__str__()` method.
**Example:**
```py
def slot_function(self, ctx: SlotContext):
return f"Hello, {ctx.fallback}!"
```
"""
""" # noqa: E501
def __init__(self, slot: "SlotNode", context: Context):
self._slot = slot
@ -400,6 +426,9 @@ class SlotFallback:
# TODO_v1 - REMOVE - superseded by SlotFallback
SlotRef = SlotFallback
"""
DEPRECATED: Use [`SlotFallback`](../api#django_components.SlotFallback) instead. Will be removed in v1.
"""
name_escape_re = re.compile(r"[^\w]")
@ -457,7 +486,7 @@ class SlotNode(BaseNode):
**Example:**
```python
```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@ -472,7 +501,7 @@ class SlotNode(BaseNode):
\"\"\"
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@ -490,12 +519,14 @@ class SlotNode(BaseNode):
\"\"\"
```
### Passing data to slots
### Slot data
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
tag via fill's `data` kwarg:
```python
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@ -508,7 +539,7 @@ class SlotNode(BaseNode):
\"\"\"
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@ -523,7 +554,7 @@ class SlotNode(BaseNode):
\"\"\"
```
### Accessing fallback slot content
### Slot fallback
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
will be rendered if no fill is given for the slot.
@ -532,7 +563,7 @@ class SlotNode(BaseNode):
the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
```python
```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@ -544,7 +575,7 @@ class SlotNode(BaseNode):
\"\"\"
```
```python
```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@ -910,21 +941,20 @@ class FillNode(BaseNode):
"""
Use this tag to insert content into component's slots.
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block.
Runtime checks should prohibit other usages.
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block,
and raises a `TemplateSyntaxError` if used outside of a component.
**Args:**
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
the default slot.
- `fallback` (str, optional): This argument allows you to access the original content of the slot
under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
- `data` (str, optional): This argument allows you to access the data passed to the slot
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
- `fallback` (str, optional): This argument allows you to access the original content of the slot
under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
**Examples:**
**Example:**
Basic usage:
```django
{% component "my_table" %}
{% fill "pagination" %}
@ -933,7 +963,15 @@ class FillNode(BaseNode):
{% endcomponent %}
```
### Accessing slot's fallback content with the `fallback` kwarg
### Access slot fallback
Use the `fallback` kwarg to access the original content of the slot.
The `fallback` kwarg defines the name of the variable that will contain the slot's fallback content.
Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
Component template:
```django
{# my_table.html #}
@ -945,6 +983,8 @@ class FillNode(BaseNode):
</table>
```
Fill:
```django
{% component "my_table" %}
{% fill "pagination" fallback="fallback" %}
@ -955,7 +995,15 @@ class FillNode(BaseNode):
{% endcomponent %}
```
### Accessing slot's data with the `data` kwarg
### Access slot data
Use the `data` kwarg to access the data passed to the slot.
The `data` kwarg defines the name of the variable that will contain the slot's data.
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
Component template:
```django
{# my_table.html #}
@ -967,6 +1015,8 @@ class FillNode(BaseNode):
</table>
```
Fill:
```django
{% component "my_table" %}
{% fill "pagination" data="slot_data" %}
@ -979,7 +1029,7 @@ class FillNode(BaseNode):
{% endcomponent %}
```
### Accessing slot data and fallback content on the default slot
### Using default slot
To access slot data and the fallback slot content on the default slot,
use `{% fill %}` with `name` set to `"default"`:
@ -993,7 +1043,7 @@ class FillNode(BaseNode):
{% endcomponent %}
```
### Passing slot fill from Python
### Slot fills from Python
You can pass a slot fill from Python to a component by setting the `body` kwarg
on the `{% fill %}` tag.
@ -1189,6 +1239,7 @@ class FillNode(BaseNode):
#######################################
# EXTRACTING {% fill %} FROM TEMPLATES
# (internal)
#######################################

View file

@ -116,6 +116,15 @@ class TestSlot:
slots={"first": "SLOT_FN"},
)
def test_render_raises_on_slot_instance_in_slot_constructor(self):
slot: Slot = Slot(lambda ctx: "SLOT_FN")
with pytest.raises(
ValueError,
match=re.escape("Slot received another Slot instance as `contents`"),
):
Slot(slot)
def test_render_slot_in_python__minimal(self):
def slot_fn(ctx: SlotContext):
assert ctx.context is None