mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 12:19:44 +00:00
parent
7a49a7806c
commit
f069255b64
12 changed files with 404 additions and 72 deletions
92
CHANGELOG.md
92
CHANGELOG.md
|
@ -236,15 +236,103 @@
|
||||||
After:
|
After:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
slot = Slot(contents=lambda *a, **kw: "CONTENT")
|
slot = Slot(contents=lambda ctx: "CONTENT")
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, pass the function / content as first positional argument:
|
Alternatively, pass the function / content as first positional argument:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
slot = Slot(lambda *a, **kw: "CONTENT")
|
slot = Slot(lambda ctx: "CONTENT")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Slot functions behavior has changed. See the new [Slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/) docs for more info.
|
||||||
|
|
||||||
|
- Function signature:
|
||||||
|
|
||||||
|
1. All parameters are now passed under a single `ctx` argument.
|
||||||
|
|
||||||
|
You can still access all the same parameters via `ctx.context`, `ctx.data`, and `ctx.fallback`.
|
||||||
|
|
||||||
|
2. `context` and `fallback` now may be `None` if the slot function was called outside of `{% slot %}` tag.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef):
|
||||||
|
isinstance(context, Context)
|
||||||
|
isinstance(data, Dict)
|
||||||
|
isinstance(slot_ref, SlotRef)
|
||||||
|
|
||||||
|
return "CONTENT"
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def slot_fn(ctx: SlotContext):
|
||||||
|
assert isinstance(ctx.context, Context) # May be None
|
||||||
|
assert isinstance(ctx.data, Dict)
|
||||||
|
assert isinstance(ctx.fallback, SlotFallback) # May be None
|
||||||
|
|
||||||
|
return "CONTENT"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Calling slot functions:
|
||||||
|
|
||||||
|
1. Rather than calling the slot functions directly, you should now call the `Slot` instances.
|
||||||
|
|
||||||
|
2. All parameters are now optional.
|
||||||
|
|
||||||
|
3. The order of parameters has changed.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef):
|
||||||
|
return "CONTENT"
|
||||||
|
|
||||||
|
html = slot_fn(context, data, slot_ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def slot_fn(ctx: SlotContext):
|
||||||
|
return "CONTENT"
|
||||||
|
|
||||||
|
slot = Slot(slot_fn)
|
||||||
|
html = slot()
|
||||||
|
html = slot({"data1": "abc", "data2": "hello"})
|
||||||
|
html = slot({"data1": "abc", "data2": "hello"}, fallback="FALLBACK")
|
||||||
|
```
|
||||||
|
|
||||||
|
- Usage in components:
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
slots = self.input.slots
|
||||||
|
slot_fn = slots["my_slot"]
|
||||||
|
html = slot_fn(context, data, slot_ref)
|
||||||
|
return {
|
||||||
|
"html": html,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
slot_fn = slots["my_slot"]
|
||||||
|
html = slot_fn(data)
|
||||||
|
return {
|
||||||
|
"html": html,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Miscellaneous**
|
**Miscellaneous**
|
||||||
|
|
||||||
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.
|
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.
|
||||||
|
|
|
@ -131,7 +131,7 @@ MyComponent.render(
|
||||||
```py
|
```py
|
||||||
MyComponent.render(
|
MyComponent.render(
|
||||||
kwargs={"position": "left"},
|
kwargs={"position": "left"},
|
||||||
slots={"content": lambda *a, **kwa: "Hello, Alice"}
|
slots={"content": lambda ctx: "Hello, Alice"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -513,7 +513,7 @@ Button.render(
|
||||||
age=30,
|
age=30,
|
||||||
),
|
),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
footer=Slot(lambda *a, **kwa: "Click me!"),
|
footer=Slot(lambda ctx: "Click me!"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
|
@ -128,7 +128,7 @@ Button.render(
|
||||||
age=30,
|
age=30,
|
||||||
),
|
),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
footer=Slot(lambda *a, **kwa: "Click me!"),
|
footer=Slot(lambda ctx: "Click me!"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -601,7 +601,7 @@ Button.render(
|
||||||
},
|
},
|
||||||
slots={
|
slots={
|
||||||
"my_slot": "...",
|
"my_slot": "...",
|
||||||
"another_slot": Slot(lambda: ...),
|
"another_slot": Slot(lambda ctx: ...),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -662,7 +662,7 @@ Button.render(
|
||||||
),
|
),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
my_slot="...",
|
my_slot="...",
|
||||||
another_slot=Slot(lambda: ...),
|
another_slot=Slot(lambda ctx: ...),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
|
@ -50,7 +50,16 @@ from django_components.extensions.defaults import ComponentDefaults, Default
|
||||||
from django_components.extensions.view import ComponentView, get_component_url
|
from django_components.extensions.view import ComponentView, get_component_url
|
||||||
from django_components.library import TagProtectedError
|
from django_components.library import TagProtectedError
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
from django_components.slots import Slot, SlotContent, SlotFallback, SlotFunc, SlotInput, SlotRef, SlotResult
|
from django_components.slots import (
|
||||||
|
Slot,
|
||||||
|
SlotContent,
|
||||||
|
SlotContext,
|
||||||
|
SlotFallback,
|
||||||
|
SlotFunc,
|
||||||
|
SlotInput,
|
||||||
|
SlotRef,
|
||||||
|
SlotResult,
|
||||||
|
)
|
||||||
from django_components.tag_formatter import (
|
from django_components.tag_formatter import (
|
||||||
ComponentFormatter,
|
ComponentFormatter,
|
||||||
ShorthandComponentFormatter,
|
ShorthandComponentFormatter,
|
||||||
|
@ -125,6 +134,7 @@ __all__ = [
|
||||||
"ShorthandComponentFormatter",
|
"ShorthandComponentFormatter",
|
||||||
"Slot",
|
"Slot",
|
||||||
"SlotContent",
|
"SlotContent",
|
||||||
|
"SlotContext",
|
||||||
"SlotFallback",
|
"SlotFallback",
|
||||||
"SlotFunc",
|
"SlotFunc",
|
||||||
"SlotInput",
|
"SlotInput",
|
||||||
|
|
|
@ -458,7 +458,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
Table.render(
|
Table.render(
|
||||||
slots=Table.Slots(
|
slots=Table.Slots(
|
||||||
header="HELLO IM HEADER",
|
header="HELLO IM HEADER",
|
||||||
footer=Slot(lambda: ...),
|
footer=Slot(lambda ctx: ...),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -2132,8 +2132,8 @@ class Component(metaclass=ComponentMeta):
|
||||||
Button.render(
|
Button.render(
|
||||||
slots={
|
slots={
|
||||||
"content": "Click me!"
|
"content": "Click me!"
|
||||||
"content2": lambda *a, **kwa: "Click me!",
|
"content2": lambda ctx: "Click me!",
|
||||||
"content3": Slot(lambda *a, **kwa: "Click me!"),
|
"content3": Slot(lambda ctx: "Click me!"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
@ -2260,7 +2260,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
age=30,
|
age=30,
|
||||||
),
|
),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
footer=Slot(lambda *a, **kwa: "Click me!"),
|
footer=Slot(lambda ctx: "Click me!"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
|
@ -38,7 +38,7 @@ from django_components.util.misc import get_index, get_last_index, is_identifier
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import ComponentContext, ComponentNode
|
from django_components.component import ComponentContext, ComponentNode
|
||||||
|
|
||||||
TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
|
TSlotData = TypeVar("TSlotData", bound=Mapping)
|
||||||
|
|
||||||
DEFAULT_SLOT_KEY = "default"
|
DEFAULT_SLOT_KEY = "default"
|
||||||
FILL_GEN_CONTEXT_KEY = "_DJANGO_COMPONENTS_GEN_FILL"
|
FILL_GEN_CONTEXT_KEY = "_DJANGO_COMPONENTS_GEN_FILL"
|
||||||
|
@ -54,32 +54,193 @@ SlotResult = Union[str, SafeString]
|
||||||
"""Type representing the result of a slot render function."""
|
"""Type representing the result of a slot render function."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SlotContext(Generic[TSlotData]):
|
||||||
|
"""
|
||||||
|
Metadata available inside slot functions.
|
||||||
|
|
||||||
|
Read more about [Slot functions](../../concepts/fundamentals/slots#slot-class).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import SlotContext
|
||||||
|
|
||||||
|
def my_slot(ctx: SlotContext):
|
||||||
|
return f"Hello, {ctx.data['name']}!"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can pass a type parameter to the `SlotContext` to specify the type of the data passed to the slot:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MySlotData(TypedDict):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def my_slot(ctx: SlotContext[MySlotData]):
|
||||||
|
return f"Hello, {ctx.data['name']}!"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: TSlotData
|
||||||
|
"""
|
||||||
|
Data passed to the slot.
|
||||||
|
|
||||||
|
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_slot(ctx: SlotContext):
|
||||||
|
return f"Hello, {ctx.data['name']}!"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
fallback: Optional[Union[str, "SlotFallback"]] = None
|
||||||
|
"""
|
||||||
|
Slot's fallback content. Lazily-rendered - coerce this value to string to force it to render.
|
||||||
|
|
||||||
|
Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def my_slot(ctx: SlotContext):
|
||||||
|
return f"Hello, {ctx.fallback}!"
|
||||||
|
```
|
||||||
|
|
||||||
|
May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
|
||||||
|
"""
|
||||||
|
context: Optional[Context] = None
|
||||||
|
"""
|
||||||
|
Django template [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
|
||||||
|
available inside the [`{% fill %}`](../template_tags#fill) tag.
|
||||||
|
|
||||||
|
May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class SlotFunc(Protocol, Generic[TSlotData]):
|
class SlotFunc(Protocol, Generic[TSlotData]):
|
||||||
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult: ... # noqa E704
|
"""
|
||||||
|
When rendering components with
|
||||||
|
[`Component.render()`](../api#django_components.Component.render)
|
||||||
|
or
|
||||||
|
[`Component.render_to_response()`](../api#django_components.Component.render_to_response),
|
||||||
|
the slots can be given either as strings or as functions.
|
||||||
|
|
||||||
|
If a slot is given as a function, it will have the signature of `SlotFunc`.
|
||||||
|
|
||||||
|
Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx (SlotContext): Single named tuple that holds the slot data and metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(str | SafeString): The rendered slot content.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import SlotContext, SlotResult
|
||||||
|
|
||||||
|
def header(ctx: SlotContext) -> SlotResult:
|
||||||
|
if ctx.data.get("name"):
|
||||||
|
return f"Hello, {ctx.data['name']}!"
|
||||||
|
else:
|
||||||
|
return ctx.fallback
|
||||||
|
|
||||||
|
html = MyTable.render(
|
||||||
|
slots={
|
||||||
|
"header": header,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, ctx: SlotContext[TSlotData]) -> SlotResult: ... # noqa E704
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Slot(Generic[TSlotData]):
|
class Slot(Generic[TSlotData]):
|
||||||
"""This class holds the slot content function along with related metadata."""
|
"""
|
||||||
|
This class is the main way for defining and handling slots.
|
||||||
|
|
||||||
|
It holds the slot content function along with related metadata.
|
||||||
|
|
||||||
|
Read more about [Slot class](../../concepts/fundamentals/slots#slot-class).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
Passing slots to components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import Slot
|
||||||
|
|
||||||
|
slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
|
||||||
|
|
||||||
|
MyComponent.render(
|
||||||
|
slots={
|
||||||
|
"my_slot": slot,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing slots inside the components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
my_slot = slots["my_slot"]
|
||||||
|
return {
|
||||||
|
"my_slot": my_slot,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendering slots:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import Slot
|
||||||
|
|
||||||
|
slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
|
||||||
|
html = slot({"name": "John"}) # Output: Hello, John!
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
contents: Any
|
contents: Any
|
||||||
"""
|
"""
|
||||||
The original value that was passed to the `Slot` constructor.
|
The original value that was passed to the `Slot` constructor.
|
||||||
|
|
||||||
If the slot was defined with `{% fill %}` tag, this will be the raw string contents of the slot.
|
- If Slot was created from [`{% fill %}`](../template_tags#fill) tag, `Slot.contents` will contain
|
||||||
|
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).
|
||||||
"""
|
"""
|
||||||
content_func: SlotFunc[TSlotData] = cast(SlotFunc[TSlotData], None)
|
content_func: SlotFunc[TSlotData] = cast(SlotFunc[TSlotData], None)
|
||||||
|
"""
|
||||||
|
The actual slot function.
|
||||||
|
|
||||||
|
Do NOT call this function directly, instead call the `Slot` instance as a function.
|
||||||
|
|
||||||
|
Read more about [Rendering slot functions](../../concepts/fundamentals/slots#rendering-slots).
|
||||||
|
"""
|
||||||
escaped: bool = False
|
escaped: bool = False
|
||||||
"""Whether the slot content has been escaped."""
|
"""Whether the slot content has been escaped."""
|
||||||
|
|
||||||
# Following fields are only for debugging
|
# Following fields are only for debugging
|
||||||
component_name: Optional[str] = None
|
component_name: Optional[str] = None
|
||||||
"""Name of the component that originally defined or accepted this slot fill."""
|
"""Name of the component that originally received this slot fill."""
|
||||||
slot_name: Optional[str] = None
|
slot_name: Optional[str] = None
|
||||||
"""Name of the slot that originally defined or accepted this slot fill."""
|
"""Slot name to which this Slot was initially assigned."""
|
||||||
nodelist: Optional[NodeList] = None
|
nodelist: Optional[NodeList] = None
|
||||||
"""If the slot was defined with `{% fill %} tag, this will be the Nodelist of the slot content."""
|
"""
|
||||||
|
If the slot was defined with [`{% fill %}`](../template_tags#fill) tag,
|
||||||
|
this will be the Nodelist of the fill's content.
|
||||||
|
"""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
# Since the `Slot` instance is treated as a function, it may be passed as `contents`
|
# Since the `Slot` instance is treated as a function, it may be passed as `contents`
|
||||||
|
@ -104,13 +265,23 @@ class Slot(Generic[TSlotData]):
|
||||||
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
|
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
|
||||||
|
|
||||||
# Allow to treat the instances as functions
|
# Allow to treat the instances as functions
|
||||||
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult:
|
def __call__(
|
||||||
return self.content_func(ctx, slot_data, slot_ref)
|
self,
|
||||||
|
data: Optional[TSlotData] = None,
|
||||||
|
fallback: Optional[Union[str, "SlotFallback"]] = None,
|
||||||
|
context: Optional[Context] = None,
|
||||||
|
) -> SlotResult:
|
||||||
|
slot_ctx: SlotContext = SlotContext(context=context, data=data or {}, fallback=fallback)
|
||||||
|
return self.content_func(slot_ctx)
|
||||||
|
|
||||||
# Make Django pass the instances of this class within the templates without calling
|
# Make Django pass the instances of this class within the templates without calling
|
||||||
# the instances as a function.
|
# the instances as a function.
|
||||||
@property
|
@property
|
||||||
def do_not_call_in_templates(self) -> bool:
|
def do_not_call_in_templates(self) -> bool:
|
||||||
|
"""
|
||||||
|
Django special property to prevent calling the instance as a function
|
||||||
|
inside Django templates.
|
||||||
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -177,10 +348,10 @@ html = Table.render(
|
||||||
"header": mark_safe("<i><am><safe>"),
|
"header": mark_safe("<i><am><safe>"),
|
||||||
|
|
||||||
# Function
|
# Function
|
||||||
"footer": lambda ctx, slot_data, slot_ref: f"Page: {slot_data['page_number']}!",
|
"footer": lambda ctx: f"Page: {ctx.data['page_number']}!",
|
||||||
|
|
||||||
# Slot instance
|
# Slot instance
|
||||||
"footer": Slot(lambda ctx, slot_data, slot_ref: f"Page: {slot_data['page_number']}!"),
|
"footer": Slot(lambda ctx: f"Page: {ctx.data['page_number']}!"),
|
||||||
|
|
||||||
# None (Same as no slot)
|
# None (Same as no slot)
|
||||||
"header": None,
|
"header": None,
|
||||||
|
@ -211,8 +382,8 @@ class SlotFallback:
|
||||||
Usage in slot functions:
|
Usage in slot functions:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def slot_function(self, ctx: Context, slot_data: TSlotData, fallback: SlotFallback):
|
def slot_function(self, ctx: SlotContext):
|
||||||
return f"Hello, {fallback}!"
|
return f"Hello, {ctx.fallback}!"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -637,7 +808,7 @@ class SlotNode(BaseNode):
|
||||||
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||||
extra_context[key] = value
|
extra_context[key] = value
|
||||||
|
|
||||||
slot_ref = SlotFallback(self, context)
|
fallback = SlotFallback(self, context)
|
||||||
|
|
||||||
# For the user-provided slot fill, we want to use the context of where the slot
|
# For the user-provided slot fill, we want to use the context of where the slot
|
||||||
# came from (or current context if configured so)
|
# came from (or current context if configured so)
|
||||||
|
@ -658,7 +829,7 @@ class SlotNode(BaseNode):
|
||||||
# Render slot as a function
|
# Render slot as a function
|
||||||
# NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
|
# NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
|
||||||
# the render function ALWAYS receives them.
|
# the render function ALWAYS receives them.
|
||||||
output = slot(used_ctx, kwargs, slot_ref)
|
output = slot(data=kwargs, fallback=fallback, context=used_ctx)
|
||||||
|
|
||||||
if app_settings.DEBUG_HIGHLIGHT_SLOTS:
|
if app_settings.DEBUG_HIGHLIGHT_SLOTS:
|
||||||
output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
|
output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
|
||||||
|
@ -1119,7 +1290,7 @@ def normalize_slot_fills(
|
||||||
|
|
||||||
# NOTE: `gen_escaped_content_func` is defined as a separate function, instead of being inlined within
|
# NOTE: `gen_escaped_content_func` is defined as a separate function, instead of being inlined within
|
||||||
# the forloop, because the value the forloop variable points to changes with each loop iteration.
|
# the forloop, because the value the forloop variable points to changes with each loop iteration.
|
||||||
def gen_escaped_content_func(content: SlotFunc, slot_name: str) -> Slot:
|
def gen_escaped_content_func(content: Union[SlotFunc, Slot], slot_name: str) -> Slot:
|
||||||
# Case: Already Slot, already escaped, and names assigned, so nothing to do.
|
# Case: Already Slot, already escaped, and names assigned, so nothing to do.
|
||||||
if isinstance(content, Slot) and content.escaped and content.slot_name and content.component_name:
|
if isinstance(content, Slot) and content.escaped and content.slot_name and content.component_name:
|
||||||
return content
|
return content
|
||||||
|
@ -1128,8 +1299,8 @@ def normalize_slot_fills(
|
||||||
# so we can assign metadata to our internal copies.
|
# so we can assign metadata to our internal copies.
|
||||||
if not isinstance(content, Slot) or not content.escaped:
|
if not isinstance(content, Slot) or not content.escaped:
|
||||||
# We wrap the original function so we post-process it by escaping the result.
|
# We wrap the original function so we post-process it by escaping the result.
|
||||||
def content_fn(ctx: Context, slot_data: Dict, fallback: SlotFallback) -> SlotResult:
|
def content_fn(ctx: SlotContext) -> SlotResult:
|
||||||
rendered = content(ctx, slot_data, fallback)
|
rendered = content(ctx)
|
||||||
return conditional_escape(rendered) if escape_content else rendered
|
return conditional_escape(rendered) if escape_content else rendered
|
||||||
|
|
||||||
content_func = cast(SlotFunc, content_fn)
|
content_func = cast(SlotFunc, content_fn)
|
||||||
|
@ -1227,17 +1398,19 @@ def _nodelist_to_slot(
|
||||||
# This allows the template to access current RenderContext layer.
|
# This allows the template to access current RenderContext layer.
|
||||||
template._djc_is_component_nested = True
|
template._djc_is_component_nested = True
|
||||||
|
|
||||||
def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotFallback) -> SlotResult:
|
def render_func(ctx: SlotContext) -> SlotResult:
|
||||||
|
context = ctx.context or Context()
|
||||||
|
|
||||||
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
|
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
|
||||||
# are made available through a variable name that was set on the `{% fill %}`
|
# are made available through a variable name that was set on the `{% fill %}`
|
||||||
# tag.
|
# tag.
|
||||||
if data_var:
|
if data_var:
|
||||||
ctx[data_var] = slot_data
|
context[data_var] = ctx.data
|
||||||
|
|
||||||
# If slot fill is using `{% fill "myslot" fallback="abc" %}`, then set the "abc" to
|
# If slot fill is using `{% fill "myslot" fallback="abc" %}`, then set the "abc" to
|
||||||
# the context, so users can refer to the fallback slot from within the fill content.
|
# the context, so users can refer to the fallback slot from within the fill content.
|
||||||
if fallback_var:
|
if fallback_var:
|
||||||
ctx[fallback_var] = slot_ref
|
context[fallback_var] = ctx.fallback
|
||||||
|
|
||||||
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
|
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
|
||||||
# the `extra_context` contains the forloop variables. We want to make these available
|
# the `extra_context` contains the forloop variables. We want to make these available
|
||||||
|
@ -1260,7 +1433,7 @@ def _nodelist_to_slot(
|
||||||
# HOWEVER, the layer with `_COMPONENT_CONTEXT_KEY` also contains user-defined data from `get_template_data()`.
|
# HOWEVER, the layer with `_COMPONENT_CONTEXT_KEY` also contains user-defined data from `get_template_data()`.
|
||||||
# Data from `get_template_data()` should take precedence over `extra_context`. So we have to insert
|
# Data from `get_template_data()` should take precedence over `extra_context`. So we have to insert
|
||||||
# the forloop variables BEFORE that.
|
# the forloop variables BEFORE that.
|
||||||
index_of_last_component_layer = get_last_index(ctx.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
|
index_of_last_component_layer = get_last_index(context.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
|
||||||
if index_of_last_component_layer is None:
|
if index_of_last_component_layer is None:
|
||||||
index_of_last_component_layer = 0
|
index_of_last_component_layer = 0
|
||||||
|
|
||||||
|
@ -1272,18 +1445,18 @@ def _nodelist_to_slot(
|
||||||
|
|
||||||
# Insert the `extra_context` layer BEFORE the layer that defines the variables from get_template_data.
|
# Insert the `extra_context` layer BEFORE the layer that defines the variables from get_template_data.
|
||||||
# Thus, get_template_data will overshadow these on conflict.
|
# Thus, get_template_data will overshadow these on conflict.
|
||||||
ctx.dicts.insert(index_of_last_component_layer, extra_context or {})
|
context.dicts.insert(index_of_last_component_layer, extra_context or {})
|
||||||
|
|
||||||
trace_component_msg("RENDER_NODELIST", component_name, component_id=None, slot_name=slot_name)
|
trace_component_msg("RENDER_NODELIST", component_name, component_id=None, slot_name=slot_name)
|
||||||
|
|
||||||
# We wrap the slot nodelist in Template. However, we also override Django's `Template.render()`
|
# We wrap the slot nodelist in Template. However, we also override Django's `Template.render()`
|
||||||
# to call `render_dependencies()` on the results. So we need to set the strategy to `ignore`
|
# to call `render_dependencies()` on the results. So we need to set the strategy to `ignore`
|
||||||
# so that the dependencies are processed only once the whole component tree is rendered.
|
# so that the dependencies are processed only once the whole component tree is rendered.
|
||||||
with ctx.push({"DJC_DEPS_STRATEGY": "ignore"}):
|
with context.push({"DJC_DEPS_STRATEGY": "ignore"}):
|
||||||
rendered = template.render(ctx)
|
rendered = template.render(context)
|
||||||
|
|
||||||
# After the rendering is done, remove the `extra_context` from the context stack
|
# After the rendering is done, remove the `extra_context` from the context stack
|
||||||
ctx.dicts.pop(index_of_last_component_layer)
|
context.dicts.pop(index_of_last_component_layer)
|
||||||
|
|
||||||
return rendered
|
return rendered
|
||||||
|
|
||||||
|
|
|
@ -322,7 +322,8 @@ class TestComponentRenderAPI:
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||||
assert isinstance(self.input.context, Context)
|
assert isinstance(self.input.context, Context)
|
||||||
assert list(self.input.slots.keys()) == ["my_slot"]
|
assert list(self.input.slots.keys()) == ["my_slot"]
|
||||||
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
|
my_slot = self.input.slots["my_slot"]
|
||||||
|
assert my_slot() == "MY_SLOT"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"variable": kwargs["variable"],
|
"variable": kwargs["variable"],
|
||||||
|
@ -334,7 +335,8 @@ class TestComponentRenderAPI:
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||||
assert isinstance(self.input.context, Context)
|
assert isinstance(self.input.context, Context)
|
||||||
assert list(self.input.slots.keys()) == ["my_slot"]
|
assert list(self.input.slots.keys()) == ["my_slot"]
|
||||||
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
|
my_slot = self.input.slots["my_slot"]
|
||||||
|
assert my_slot() == "MY_SLOT"
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
|
|
@ -346,5 +346,5 @@ class TestComponentCache:
|
||||||
):
|
):
|
||||||
TestComponent.render(
|
TestComponent.render(
|
||||||
kwargs={"input": "cake"},
|
kwargs={"input": "cake"},
|
||||||
slots={"content": lambda *a, **kwa: "ONE"},
|
slots={"content": lambda ctx: "ONE"},
|
||||||
)
|
)
|
||||||
|
|
|
@ -76,7 +76,7 @@ class TestComponentTyping:
|
||||||
kwargs=Button.Kwargs(name="name", age=123),
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
header="HEADER",
|
header="HEADER",
|
||||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
footer=Slot(lambda ctx: "FOOTER"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ class TestComponentTyping:
|
||||||
kwargs={"name": "name", "age": 123},
|
kwargs={"name": "name", "age": 123},
|
||||||
slots={
|
slots={
|
||||||
"header": "HEADER",
|
"header": "HEADER",
|
||||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
"footer": Slot(lambda ctx: "FOOTER"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ class TestComponentTyping:
|
||||||
kwargs={"name": "name", "age": 123},
|
kwargs={"name": "name", "age": 123},
|
||||||
slots={
|
slots={
|
||||||
"header": "HEADER",
|
"header": "HEADER",
|
||||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
"footer": Slot(lambda ctx: "FOOTER"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -314,7 +314,7 @@ class TestComponentTyping:
|
||||||
kwargs={"name": "name", "age": 123},
|
kwargs={"name": "name", "age": 123},
|
||||||
slots={
|
slots={
|
||||||
"header": "HEADER",
|
"header": "HEADER",
|
||||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
"footer": Slot(lambda ctx: "FOOTER"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -397,7 +397,7 @@ class TestComponentTyping:
|
||||||
kwargs=Button.Kwargs(name="name", age=123),
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
header="HEADER",
|
header="HEADER",
|
||||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
footer=Slot(lambda ctx: "FOOTER"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -412,7 +412,7 @@ class TestComponentTyping:
|
||||||
kwargs=Button.Kwargs(name="name", age=123),
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
header="HEADER",
|
header="HEADER",
|
||||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
footer=Slot(lambda ctx: "FOOTER"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -427,7 +427,7 @@ class TestComponentTyping:
|
||||||
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
|
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
|
||||||
slots=Button.Slots(
|
slots=Button.Slots(
|
||||||
header="HEADER",
|
header="HEADER",
|
||||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
footer=Slot(lambda ctx: "FOOTER"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -438,7 +438,7 @@ class TestComponentTyping:
|
||||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
kwargs=Button.Kwargs(name="name", age=123),
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
slots=Button.Slots( # type: ignore[typeddict-item]
|
slots=Button.Slots( # type: ignore[typeddict-item]
|
||||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"), # Missing header
|
footer=Slot(lambda ctx: "FOOTER"), # Missing header
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ For tests focusing on the `{% slot %}` tag, see `test_templatetags_slot_fill.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
@ -12,7 +11,7 @@ from django.template.base import NodeList, TextNode
|
||||||
from pytest_django.asserts import assertHTMLEqual
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
from django_components.slots import Slot, SlotFallback
|
from django_components.slots import Slot, SlotContext, SlotFallback
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
@ -49,43 +48,44 @@ class TestSlot:
|
||||||
"kwargs": kwargs,
|
"kwargs": kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotFallback):
|
def slot_fn(ctx: SlotContext):
|
||||||
assert isinstance(ctx, Context)
|
context = ctx.context
|
||||||
|
assert isinstance(context, Context)
|
||||||
# NOTE: Since the slot has access to the Context object, it should behave
|
# NOTE: Since the slot has access to the Context object, it should behave
|
||||||
# the same way as it does in templates - when in "isolated" mode, then the
|
# the same way as it does in templates - when in "isolated" mode, then the
|
||||||
# slot fill has access only to the "root" context, but not to the data of
|
# slot fill has access only to the "root" context, but not to the data of
|
||||||
# get_template_data() of SimpleComponent.
|
# get_template_data() of SimpleComponent.
|
||||||
if is_isolated:
|
if is_isolated:
|
||||||
assert ctx.get("the_arg") is None
|
assert context.get("the_arg") is None
|
||||||
assert ctx.get("the_kwarg") is None
|
assert context.get("the_kwarg") is None
|
||||||
assert ctx.get("kwargs") is None
|
assert context.get("kwargs") is None
|
||||||
assert ctx.get("abc") is None
|
assert context.get("abc") is None
|
||||||
else:
|
else:
|
||||||
assert ctx["the_arg"] == "1"
|
assert context["the_arg"] == "1"
|
||||||
assert ctx["the_kwarg"] == 3
|
assert context["the_kwarg"] == 3
|
||||||
assert ctx["kwargs"] == {}
|
assert context["kwargs"] == {}
|
||||||
assert ctx["abc"] == "def"
|
assert context["abc"] == "def"
|
||||||
|
|
||||||
slot_data_expected = {
|
slot_data_expected = {
|
||||||
"data1": "abc",
|
"data1": "abc",
|
||||||
"data2": {"hello": "world", "one": 123},
|
"data2": {"hello": "world", "one": 123},
|
||||||
}
|
}
|
||||||
assert slot_data_expected == slot_data
|
assert slot_data_expected == ctx.data
|
||||||
|
|
||||||
assert isinstance(slot_ref, SlotFallback)
|
assert isinstance(ctx.fallback, SlotFallback)
|
||||||
assert "SLOT_DEFAULT" == str(slot_ref).strip()
|
assert "SLOT_DEFAULT" == str(ctx.fallback).strip()
|
||||||
|
|
||||||
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
|
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
|
||||||
|
|
||||||
rendered = SimpleComponent.render(
|
rendered = SimpleComponent.render(
|
||||||
context={"abc": "def"},
|
context={"abc": "def"},
|
||||||
args=["1"],
|
args=["1"],
|
||||||
kwargs={"the_kwarg": 3},
|
kwargs={"the_kwarg": 3},
|
||||||
slots={"first": first_slot},
|
slots={"first": slot_fn},
|
||||||
)
|
)
|
||||||
assertHTMLEqual(
|
assertHTMLEqual(
|
||||||
rendered,
|
rendered,
|
||||||
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
||||||
)
|
)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
@ -116,7 +116,66 @@ class TestSlot:
|
||||||
)
|
)
|
||||||
|
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
slots={"first": "FIRST_SLOT"},
|
slots={"first": "SLOT_FN"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_render_slot_in_python__minimal(self):
|
||||||
|
def slot_fn(ctx: SlotContext):
|
||||||
|
assert ctx.context is None
|
||||||
|
assert ctx.data == {}
|
||||||
|
assert ctx.fallback is None
|
||||||
|
|
||||||
|
return "FROM_INSIDE_SLOT_FN"
|
||||||
|
|
||||||
|
slot: Slot = Slot(slot_fn)
|
||||||
|
rendered = slot()
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"FROM_INSIDE_SLOT_FN",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_render_slot_in_python__with_data(self):
|
||||||
|
def slot_fn(ctx: SlotContext):
|
||||||
|
assert ctx.context is not None
|
||||||
|
assert ctx.context["the_arg"] == "1"
|
||||||
|
assert ctx.context["the_kwarg"] == 3
|
||||||
|
assert ctx.context["kwargs"] == {}
|
||||||
|
assert ctx.context["abc"] == "def"
|
||||||
|
|
||||||
|
slot_data_expected = {
|
||||||
|
"data1": "abc",
|
||||||
|
"data2": {"hello": "world", "one": 123},
|
||||||
|
}
|
||||||
|
assert slot_data_expected == ctx.data
|
||||||
|
|
||||||
|
assert isinstance(ctx.fallback, str)
|
||||||
|
assert "SLOT_DEFAULT" == ctx.fallback
|
||||||
|
|
||||||
|
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
|
||||||
|
|
||||||
|
slot: Slot = Slot(slot_fn)
|
||||||
|
context = Context({"the_arg": "1", "the_kwarg": 3, "kwargs": {}, "abc": "def"})
|
||||||
|
|
||||||
|
# Test positional arguments
|
||||||
|
rendered = slot(
|
||||||
|
{"data1": "abc", "data2": {"hello": "world", "one": 123}},
|
||||||
|
"SLOT_DEFAULT",
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test keyword arguments
|
||||||
|
rendered2 = slot(
|
||||||
|
data={"data1": "abc", "data2": {"hello": "world", "one": 123}},
|
||||||
|
fallback="SLOT_DEFAULT",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered2,
|
||||||
|
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
||||||
|
@ -180,7 +239,7 @@ class TestSlot:
|
||||||
nonlocal captured_slots
|
nonlocal captured_slots
|
||||||
captured_slots = slots
|
captured_slots = slots
|
||||||
|
|
||||||
slot_func = lambda ctx, slot_data, slot_ref: "FROM_INSIDE_SLOT" # noqa: E731
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||||
|
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
slots={"first": slot_func},
|
slots={"first": slot_func},
|
||||||
|
@ -223,7 +282,7 @@ class TestSlot:
|
||||||
nonlocal captured_slots
|
nonlocal captured_slots
|
||||||
captured_slots = slots
|
captured_slots = slots
|
||||||
|
|
||||||
slot_func = lambda ctx, slot_data, slot_ref: "FROM_INSIDE_SLOT" # noqa: E731
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||||
|
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
slots={"first": Slot(slot_func)},
|
slots={"first": Slot(slot_func)},
|
||||||
|
|
|
@ -2440,9 +2440,9 @@ class TestSlotInput:
|
||||||
|
|
||||||
assert seen_slots == {}
|
assert seen_slots == {}
|
||||||
|
|
||||||
header_slot: Slot = Slot(lambda *a, **kw: "HEADER_SLOT")
|
header_slot: Slot = Slot(lambda ctx: "HEADER_SLOT")
|
||||||
main_slot_str = "MAIN_SLOT"
|
main_slot_str = "MAIN_SLOT"
|
||||||
footer_slot_fn = lambda *a, **kw: "FOOTER_SLOT" # noqa: E731
|
footer_slot_fn = lambda ctx: "FOOTER_SLOT" # noqa: E731
|
||||||
|
|
||||||
SlottedComponent.render(
|
SlottedComponent.render(
|
||||||
slots={
|
slots={
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue