mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +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:
|
||||
|
||||
```py
|
||||
slot = Slot(contents=lambda *a, **kw: "CONTENT")
|
||||
slot = Slot(contents=lambda ctx: "CONTENT")
|
||||
```
|
||||
|
||||
Alternatively, pass the function / content as first positional argument:
|
||||
|
||||
```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**
|
||||
|
||||
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.
|
||||
|
|
|
@ -131,7 +131,7 @@ MyComponent.render(
|
|||
```py
|
||||
MyComponent.render(
|
||||
kwargs={"position": "left"},
|
||||
slots={"content": lambda *a, **kwa: "Hello, Alice"}
|
||||
slots={"content": lambda ctx: "Hello, Alice"}
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
@ -513,7 +513,7 @@ Button.render(
|
|||
age=30,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
footer=Slot(lambda *a, **kwa: "Click me!"),
|
||||
footer=Slot(lambda ctx: "Click me!"),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
|
|
@ -128,7 +128,7 @@ Button.render(
|
|||
age=30,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
footer=Slot(lambda *a, **kwa: "Click me!"),
|
||||
footer=Slot(lambda ctx: "Click me!"),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
@ -601,7 +601,7 @@ Button.render(
|
|||
},
|
||||
slots={
|
||||
"my_slot": "...",
|
||||
"another_slot": Slot(lambda: ...),
|
||||
"another_slot": Slot(lambda ctx: ...),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
@ -662,7 +662,7 @@ Button.render(
|
|||
),
|
||||
slots=Button.Slots(
|
||||
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.library import TagProtectedError
|
||||
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 (
|
||||
ComponentFormatter,
|
||||
ShorthandComponentFormatter,
|
||||
|
@ -125,6 +134,7 @@ __all__ = [
|
|||
"ShorthandComponentFormatter",
|
||||
"Slot",
|
||||
"SlotContent",
|
||||
"SlotContext",
|
||||
"SlotFallback",
|
||||
"SlotFunc",
|
||||
"SlotInput",
|
||||
|
|
|
@ -458,7 +458,7 @@ class Component(metaclass=ComponentMeta):
|
|||
Table.render(
|
||||
slots=Table.Slots(
|
||||
header="HELLO IM HEADER",
|
||||
footer=Slot(lambda: ...),
|
||||
footer=Slot(lambda ctx: ...),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
@ -2132,8 +2132,8 @@ class Component(metaclass=ComponentMeta):
|
|||
Button.render(
|
||||
slots={
|
||||
"content": "Click me!"
|
||||
"content2": lambda *a, **kwa: "Click me!",
|
||||
"content3": Slot(lambda *a, **kwa: "Click me!"),
|
||||
"content2": lambda ctx: "Click me!",
|
||||
"content3": Slot(lambda ctx: "Click me!"),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
@ -2260,7 +2260,7 @@ class Component(metaclass=ComponentMeta):
|
|||
age=30,
|
||||
),
|
||||
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:
|
||||
from django_components.component import ComponentContext, ComponentNode
|
||||
|
||||
TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
|
||||
TSlotData = TypeVar("TSlotData", bound=Mapping)
|
||||
|
||||
DEFAULT_SLOT_KEY = "default"
|
||||
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."""
|
||||
|
||||
|
||||
@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
|
||||
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
|
||||
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
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
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
|
||||
"""Whether the slot content has been escaped."""
|
||||
|
||||
# Following fields are only for debugging
|
||||
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
|
||||
"""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
|
||||
"""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:
|
||||
# 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}")
|
||||
|
||||
# Allow to treat the instances as functions
|
||||
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult:
|
||||
return self.content_func(ctx, slot_data, slot_ref)
|
||||
def __call__(
|
||||
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
|
||||
# the instances as a function.
|
||||
@property
|
||||
def do_not_call_in_templates(self) -> bool:
|
||||
"""
|
||||
Django special property to prevent calling the instance as a function
|
||||
inside Django templates.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
@ -177,10 +348,10 @@ html = Table.render(
|
|||
"header": mark_safe("<i><am><safe>"),
|
||||
|
||||
# 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
|
||||
"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)
|
||||
"header": None,
|
||||
|
@ -211,8 +382,8 @@ class SlotFallback:
|
|||
Usage in slot functions:
|
||||
|
||||
```py
|
||||
def slot_function(self, ctx: Context, slot_data: TSlotData, fallback: SlotFallback):
|
||||
return f"Hello, {fallback}!"
|
||||
def slot_function(self, ctx: SlotContext):
|
||||
return f"Hello, {ctx.fallback}!"
|
||||
```
|
||||
"""
|
||||
|
||||
|
@ -637,7 +808,7 @@ class SlotNode(BaseNode):
|
|||
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||
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
|
||||
# came from (or current context if configured so)
|
||||
|
@ -658,7 +829,7 @@ class SlotNode(BaseNode):
|
|||
# Render slot as a function
|
||||
# NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
|
||||
# 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:
|
||||
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
|
||||
# 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.
|
||||
if isinstance(content, Slot) and content.escaped and content.slot_name and content.component_name:
|
||||
return content
|
||||
|
@ -1128,8 +1299,8 @@ def normalize_slot_fills(
|
|||
# so we can assign metadata to our internal copies.
|
||||
if not isinstance(content, Slot) or not content.escaped:
|
||||
# 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:
|
||||
rendered = content(ctx, slot_data, fallback)
|
||||
def content_fn(ctx: SlotContext) -> SlotResult:
|
||||
rendered = content(ctx)
|
||||
return conditional_escape(rendered) if escape_content else rendered
|
||||
|
||||
content_func = cast(SlotFunc, content_fn)
|
||||
|
@ -1227,17 +1398,19 @@ def _nodelist_to_slot(
|
|||
# This allows the template to access current RenderContext layer.
|
||||
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
|
||||
# are made available through a variable name that was set on the `{% fill %}`
|
||||
# tag.
|
||||
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
|
||||
# the context, so users can refer to the fallback slot from within the fill content.
|
||||
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,
|
||||
# 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()`.
|
||||
# Data from `get_template_data()` should take precedence over `extra_context`. So we have to insert
|
||||
# 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:
|
||||
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.
|
||||
# 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)
|
||||
|
||||
# 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`
|
||||
# so that the dependencies are processed only once the whole component tree is rendered.
|
||||
with ctx.push({"DJC_DEPS_STRATEGY": "ignore"}):
|
||||
rendered = template.render(ctx)
|
||||
with context.push({"DJC_DEPS_STRATEGY": "ignore"}):
|
||||
rendered = template.render(context)
|
||||
|
||||
# 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
|
||||
|
||||
|
|
|
@ -322,7 +322,8 @@ class TestComponentRenderAPI:
|
|||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||
assert isinstance(self.input.context, Context)
|
||||
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 {
|
||||
"variable": kwargs["variable"],
|
||||
|
@ -334,7 +335,8 @@ class TestComponentRenderAPI:
|
|||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||
assert isinstance(self.input.context, Context)
|
||||
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 = """
|
||||
{% load component_tags %}
|
||||
|
|
|
@ -346,5 +346,5 @@ class TestComponentCache:
|
|||
):
|
||||
TestComponent.render(
|
||||
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),
|
||||
slots=Button.Slots(
|
||||
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},
|
||||
slots={
|
||||
"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},
|
||||
slots={
|
||||
"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},
|
||||
slots={
|
||||
"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),
|
||||
slots=Button.Slots(
|
||||
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),
|
||||
slots=Button.Slots(
|
||||
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]
|
||||
slots=Button.Slots(
|
||||
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"),
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
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
|
||||
from typing import Dict
|
||||
|
||||
import pytest
|
||||
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 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 .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||
|
@ -49,43 +48,44 @@ class TestSlot:
|
|||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotFallback):
|
||||
assert isinstance(ctx, Context)
|
||||
def slot_fn(ctx: SlotContext):
|
||||
context = ctx.context
|
||||
assert isinstance(context, Context)
|
||||
# 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
|
||||
# slot fill has access only to the "root" context, but not to the data of
|
||||
# get_template_data() of SimpleComponent.
|
||||
if is_isolated:
|
||||
assert ctx.get("the_arg") is None
|
||||
assert ctx.get("the_kwarg") is None
|
||||
assert ctx.get("kwargs") is None
|
||||
assert ctx.get("abc") is None
|
||||
assert context.get("the_arg") is None
|
||||
assert context.get("the_kwarg") is None
|
||||
assert context.get("kwargs") is None
|
||||
assert context.get("abc") is None
|
||||
else:
|
||||
assert ctx["the_arg"] == "1"
|
||||
assert ctx["the_kwarg"] == 3
|
||||
assert ctx["kwargs"] == {}
|
||||
assert ctx["abc"] == "def"
|
||||
assert context["the_arg"] == "1"
|
||||
assert context["the_kwarg"] == 3
|
||||
assert context["kwargs"] == {}
|
||||
assert context["abc"] == "def"
|
||||
|
||||
slot_data_expected = {
|
||||
"data1": "abc",
|
||||
"data2": {"hello": "world", "one": 123},
|
||||
}
|
||||
assert slot_data_expected == slot_data
|
||||
assert slot_data_expected == ctx.data
|
||||
|
||||
assert isinstance(slot_ref, SlotFallback)
|
||||
assert "SLOT_DEFAULT" == str(slot_ref).strip()
|
||||
assert isinstance(ctx.fallback, SlotFallback)
|
||||
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(
|
||||
context={"abc": "def"},
|
||||
args=["1"],
|
||||
kwargs={"the_kwarg": 3},
|
||||
slots={"first": first_slot},
|
||||
slots={"first": slot_fn},
|
||||
)
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
|
||||
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
|
||||
)
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
|
@ -116,7 +116,66 @@ class TestSlot:
|
|||
)
|
||||
|
||||
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.
|
||||
|
@ -180,7 +239,7 @@ class TestSlot:
|
|||
nonlocal captured_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(
|
||||
slots={"first": slot_func},
|
||||
|
@ -223,7 +282,7 @@ class TestSlot:
|
|||
nonlocal captured_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(
|
||||
slots={"first": Slot(slot_func)},
|
||||
|
|
|
@ -2440,9 +2440,9 @@ class TestSlotInput:
|
|||
|
||||
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"
|
||||
footer_slot_fn = lambda *a, **kw: "FOOTER_SLOT" # noqa: E731
|
||||
footer_slot_fn = lambda ctx: "FOOTER_SLOT" # noqa: E731
|
||||
|
||||
SlottedComponent.render(
|
||||
slots={
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue