mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 20:29:44 +00:00
feat: Pass Slots to {% fill %} with 'body' kwarg (#1203)
This commit is contained in:
parent
f069255b64
commit
d514694788
3 changed files with 178 additions and 19 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -574,6 +574,30 @@
|
||||||
- If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string.
|
- If `Slot` was created from string via `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 a function, `Slot.contents` will contain that function.
|
||||||
|
|
||||||
|
- `{% fill %}` tag now accepts `body` kwarg to pass a Slot instance to fill.
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
```
|
||||||
|
|
||||||
- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
|
- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -47,6 +47,7 @@ SLOT_REQUIRED_FLAG = "required"
|
||||||
SLOT_DEFAULT_FLAG = "default"
|
SLOT_DEFAULT_FLAG = "default"
|
||||||
FILL_DATA_KWARG = "data"
|
FILL_DATA_KWARG = "data"
|
||||||
FILL_FALLBACK_KWARG = "fallback"
|
FILL_FALLBACK_KWARG = "fallback"
|
||||||
|
FILL_BODY_KWARG = "body"
|
||||||
|
|
||||||
|
|
||||||
# Public types
|
# Public types
|
||||||
|
@ -954,6 +955,46 @@ class FillNode(BaseNode):
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Passing slot fill 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 %}
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tag = "fill"
|
tag = "fill"
|
||||||
|
@ -967,6 +1008,7 @@ class FillNode(BaseNode):
|
||||||
*,
|
*,
|
||||||
data: Optional[str] = None,
|
data: Optional[str] = None,
|
||||||
fallback: Optional[str] = None,
|
fallback: Optional[str] = None,
|
||||||
|
body: Optional[SlotInput] = None,
|
||||||
# TODO_V1: Use `fallback` kwarg instead of `default`
|
# TODO_V1: Use `fallback` kwarg instead of `default`
|
||||||
default: Optional[str] = None,
|
default: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -1015,12 +1057,19 @@ class FillNode(BaseNode):
|
||||||
f" and slot data ({FILL_DATA_KWARG}=...)"
|
f" and slot data ({FILL_DATA_KWARG}=...)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if body is not None and self.contents:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Fill '{name}' received content both through '{FILL_BODY_KWARG}' kwarg and '{{% fill %}}' body. "
|
||||||
|
f"Use only one method."
|
||||||
|
)
|
||||||
|
|
||||||
fill_data = FillWithData(
|
fill_data = FillWithData(
|
||||||
fill=self,
|
fill=self,
|
||||||
name=name,
|
name=name,
|
||||||
fallback_var=fallback,
|
fallback_var=fallback,
|
||||||
data_var=data,
|
data_var=data,
|
||||||
extra_context={},
|
extra_context={},
|
||||||
|
body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._extract_fill(context, fill_data)
|
self._extract_fill(context, fill_data)
|
||||||
|
@ -1036,10 +1085,13 @@ class FillNode(BaseNode):
|
||||||
# ...
|
# ...
|
||||||
# {% endfill %}
|
# {% endfill %}
|
||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
collected_fills: List[FillWithData] = context.get(FILL_GEN_CONTEXT_KEY, None)
|
collected_fills: Optional[List[FillWithData]] = context.get(FILL_GEN_CONTEXT_KEY, None)
|
||||||
|
|
||||||
if collected_fills is None:
|
if collected_fills is None:
|
||||||
return
|
raise RuntimeError(
|
||||||
|
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
||||||
|
"Make sure that the {% fill %} tags are nested within {% component %} tags."
|
||||||
|
)
|
||||||
|
|
||||||
# To allow using variables which were defined within the template and to which
|
# To allow using variables which were defined within the template and to which
|
||||||
# the `{% fill %}` tag has access, we need to capture those variables too.
|
# the `{% fill %}` tag has access, we need to capture those variables too.
|
||||||
|
@ -1107,6 +1159,17 @@ class FillWithData(NamedTuple):
|
||||||
fill: FillNode
|
fill: FillNode
|
||||||
name: str
|
name: str
|
||||||
"""Name of the slot to be filled, as set on the `{% fill %}` tag."""
|
"""Name of the slot to be filled, as set on the `{% fill %}` tag."""
|
||||||
|
body: Optional[SlotInput]
|
||||||
|
"""
|
||||||
|
Slot fill as set by the `body` kwarg on the `{% fill %}` tag.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
```django
|
||||||
|
{% component "mycomponent" %}
|
||||||
|
{% fill "footer" body=my_slot / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
"""
|
||||||
fallback_var: Optional[str]
|
fallback_var: Optional[str]
|
||||||
"""Name of the FALLBACK variable, as set on the `{% fill %}` tag."""
|
"""Name of the FALLBACK variable, as set on the `{% fill %}` tag."""
|
||||||
data_var: Optional[str]
|
data_var: Optional[str]
|
||||||
|
@ -1224,7 +1287,12 @@ def resolve_fills(
|
||||||
# NOTE: If slot fills are explicitly defined, we use them even if they are empty (or only whitespace).
|
# NOTE: If slot fills are explicitly defined, we use them even if they are empty (or only whitespace).
|
||||||
# This is different from the default slot, where we ignore empty content.
|
# This is different from the default slot, where we ignore empty content.
|
||||||
for fill in maybe_fills:
|
for fill in maybe_fills:
|
||||||
slots[fill.name] = _nodelist_to_slot(
|
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
|
||||||
|
if fill.body is not None:
|
||||||
|
slot_fill = fill.body if isinstance(fill.body, Slot) else Slot(fill.body)
|
||||||
|
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
||||||
|
else:
|
||||||
|
slot_fill = _nodelist_to_slot(
|
||||||
component_name=component_name,
|
component_name=component_name,
|
||||||
slot_name=fill.name,
|
slot_name=fill.name,
|
||||||
nodelist=fill.fill.nodelist,
|
nodelist=fill.fill.nodelist,
|
||||||
|
@ -1235,6 +1303,7 @@ def resolve_fills(
|
||||||
# Escaped because this was defined in the template
|
# Escaped because this was defined in the template
|
||||||
escaped=True,
|
escaped=True,
|
||||||
)
|
)
|
||||||
|
slots[fill.name] = slot_fill
|
||||||
|
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
|
|
|
@ -99,17 +99,13 @@ class TestSlot:
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TemplateSyntaxError,
|
TemplateSyntaxError,
|
||||||
match=re.escape(
|
match=re.escape("Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."),
|
||||||
"Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
SimpleComponent.render()
|
SimpleComponent.render()
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TemplateSyntaxError,
|
TemplateSyntaxError,
|
||||||
match=re.escape(
|
match=re.escape("Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."),
|
||||||
"Slot 'first' is marked as 'required' (i.e. non-optional), yet no fill is provided."
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
slots={"first": None},
|
slots={"first": None},
|
||||||
|
@ -408,3 +404,73 @@ class TestSlot:
|
||||||
assert second_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
assert second_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||||
|
|
||||||
assert first_slot_func.contents == second_slot_func.contents
|
assert first_slot_func.contents == second_slot_func.contents
|
||||||
|
|
||||||
|
def test_pass_body_to_fill__slot(self):
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "first" body=my_slot / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT")
|
||||||
|
rendered: str = template.render(Context({"my_slot": my_slot}))
|
||||||
|
|
||||||
|
assert rendered.strip() == "FROM_INSIDE_NAMED_SLOT"
|
||||||
|
|
||||||
|
def test_pass_body_to_fill__string(self):
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "first" body=my_slot / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
rendered: str = template.render(Context({"my_slot": "FROM_INSIDE_NAMED_SLOT"}))
|
||||||
|
|
||||||
|
assert rendered.strip() == "FROM_INSIDE_NAMED_SLOT"
|
||||||
|
|
||||||
|
def test_pass_body_to_fill_raises_on_body(self):
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "first" body=my_slot %}
|
||||||
|
FROM_INSIDE_NAMED_SLOT
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
TemplateSyntaxError,
|
||||||
|
match=re.escape("Fill 'first' received content both through 'body' kwarg and '{% fill %}' body."),
|
||||||
|
):
|
||||||
|
template.render(Context({"my_slot": my_slot}))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue