mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
feat: Slot.extra and Slot.source metadata (#1221)
This commit is contained in:
parent
bb129aefab
commit
fa9ae9892f
5 changed files with 356 additions and 49 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -875,17 +875,27 @@ Summary:
|
|||
|
||||
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
||||
|
||||
- `Slot` class now has a `Slot.contents` attribute, which contains the original contents:
|
||||
- `Slot` class now has 3 new metadata fields:
|
||||
|
||||
- If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag.
|
||||
- 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.
|
||||
1. `Slot.contents` attribute contains the original contents:
|
||||
|
||||
- If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag.
|
||||
- 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.
|
||||
|
||||
2. `Slot.extra` attribute where you can put arbitrary metadata about the slot.
|
||||
|
||||
3. `Slot.source` attribute tells where the slot comes from:
|
||||
|
||||
- `'template'` if the slot was created from `{% fill %}` tag.
|
||||
- `'python'` if the slot was created from string, function, or `Slot` instance.
|
||||
|
||||
See [Slot metadata](https://django-components.github.io/django-components/0.140/concepts/fundamentals/slots/#slot-metadata).
|
||||
|
||||
- `{% 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:
|
||||
First pass a `Slot` instance to the template
|
||||
with the `get_template_data()` method:
|
||||
|
||||
```python
|
||||
from django_components import component, Slot
|
||||
|
|
|
@ -453,6 +453,45 @@ class ColorLoggerExtension(ComponentExtension):
|
|||
ComponentConfig = ColorLoggerComponentConfig
|
||||
```
|
||||
|
||||
### Pass slot metadata
|
||||
|
||||
When a slot is passed to a component, it is copied, so that the original slot is not modified
|
||||
with rendering metadata.
|
||||
|
||||
Therefore, don't use slot's identity to associate metadata with the slot:
|
||||
|
||||
```py
|
||||
# ❌ Don't do this:
|
||||
slots_cache = {}
|
||||
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
for slot in ctx.component.slots.values():
|
||||
slots_cache[id(slot)] = {"some": "metadata"}
|
||||
```
|
||||
|
||||
Instead, use the [`Slot.extra`](../../../reference/api#django_components.Slot.extra) attribute,
|
||||
which is copied from the original slot:
|
||||
|
||||
```python
|
||||
# ✅ Do this:
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
# Save component-level logger settings for each slot when a component is rendered.
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
for slot in ctx.component.slots.values():
|
||||
slot.extra["logger"] = ctx.component.logger
|
||||
|
||||
# Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
|
||||
# from the slot's metadata.
|
||||
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
||||
logger = ctx.slot.extra["logger"]
|
||||
logger.log(...)
|
||||
```
|
||||
|
||||
## Extension commands
|
||||
|
||||
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
||||
|
|
|
@ -723,25 +723,52 @@ 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),
|
||||
[`slot_name`](../../../reference/api#django_components.Slot.slot_name), and
|
||||
[`nodelist`](../../../reference/api#django_components.Slot.nodelist).
|
||||
with extra metadata:
|
||||
|
||||
These are used for debugging, such as printing the path to the slot in the component tree.
|
||||
- [`component_name`](../../../reference/api#django_components.Slot.component_name)
|
||||
- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
|
||||
- [`nodelist`](../../../reference/api#django_components.Slot.nodelist)
|
||||
- [`source`](../../../reference/api#django_components.Slot.source)
|
||||
- [`extra`](../../../reference/api#django_components.Slot.extra)
|
||||
|
||||
When you create a slot, you can set these fields too:
|
||||
These are populated the first time a slot is passed to a component.
|
||||
|
||||
So if you pass the same slot through multiple nested components, the metadata will
|
||||
still point to the first component that received the slot.
|
||||
|
||||
You can use these for debugging, such as printing out the slot's component name and slot name.
|
||||
|
||||
Extensions can use [`Slot.source`](../../../reference/api#django_components.Slot.source)
|
||||
to handle slots differently based on whether the slot
|
||||
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) tag
|
||||
or in the component's Python code. See an example in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
|
||||
|
||||
You can also pass any additional data along with the slot by setting it in [`Slot.extra`](../../../reference/api#django_components.Slot.extra):
|
||||
|
||||
```py
|
||||
slot = Slot(
|
||||
lambda ctx: f"Hello, {ctx.data['name']}!",
|
||||
extra={"foo": "bar"},
|
||||
)
|
||||
```
|
||||
|
||||
When you create a slot, you can set any of these fields too:
|
||||
|
||||
```py
|
||||
# Either at slot creation
|
||||
slot = Slot(
|
||||
lambda ctx: f"Hello, {ctx.data['name']}!",
|
||||
# Optional
|
||||
component_name="table",
|
||||
slot_name="name",
|
||||
source="python",
|
||||
extra={},
|
||||
)
|
||||
|
||||
# Or later
|
||||
slot.component_name = "table"
|
||||
slot.slot_name = "name"
|
||||
slot.extra["foo"] = "bar"
|
||||
```
|
||||
|
||||
### Slot contents
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import difflib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import replace as dataclass_replace
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
@ -33,7 +34,7 @@ from django_components.node import BaseNode
|
|||
from django_components.perfutil.component import component_context_cache
|
||||
from django_components.util.exception import add_slot_to_error_message
|
||||
from django_components.util.logger import trace_component_msg
|
||||
from django_components.util.misc import get_index, get_last_index, is_identifier
|
||||
from django_components.util.misc import default, get_index, get_last_index, is_identifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component, ComponentNode
|
||||
|
@ -264,6 +265,34 @@ class Slot(Generic[TSlotData]):
|
|||
|
||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||
"""
|
||||
source: Literal["template", "python"] = "python"
|
||||
"""
|
||||
Whether the slot was created from a [`{% fill %}`](../template_tags#fill) tag (`'template'`),
|
||||
or Python (`'python'`).
|
||||
|
||||
Extensions can use this info to handle slots differently based on their source.
|
||||
|
||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||
"""
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
"""
|
||||
Dictionary that can be used to store arbitrary metadata about the slot.
|
||||
|
||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||
|
||||
See [Pass slot metadata](../../concepts/advanced/extensions#pass-slot-metadata)
|
||||
for usage for extensions.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
# Either at slot creation
|
||||
slot = Slot(lambda ctx: "Hello, world!", extra={"foo": "bar"})
|
||||
|
||||
# Or later
|
||||
slot.extra["baz"] = "qux"
|
||||
```
|
||||
"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Raise if Slot received another Slot instance as `contents`,
|
||||
|
@ -1366,6 +1395,7 @@ def resolve_fills(
|
|||
contents=contents,
|
||||
data_var=None,
|
||||
fallback_var=None,
|
||||
source="template",
|
||||
)
|
||||
|
||||
# The content has fills
|
||||
|
@ -1375,7 +1405,14 @@ def resolve_fills(
|
|||
for fill in maybe_fills:
|
||||
# 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)
|
||||
if isinstance(fill.body, Slot):
|
||||
# Make a copy of the Slot instance and set it to `source="template"`,
|
||||
# so it behaves the same as if the content was written inside the `{% fill %}` tag.
|
||||
# This for example allows CSS scoping to work even on slots that are defined
|
||||
# as `{% fill ... body=... / %}`
|
||||
slot_fill = dataclass_replace(fill.body, source="template")
|
||||
else:
|
||||
slot_fill = Slot(fill.body)
|
||||
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
||||
else:
|
||||
slot_fill = _nodelist_to_slot(
|
||||
|
@ -1386,6 +1423,7 @@ def resolve_fills(
|
|||
data_var=fill.data_var,
|
||||
fallback_var=fill.fallback_var,
|
||||
extra_context=fill.extra_context,
|
||||
source="template",
|
||||
)
|
||||
slots[fill.name] = slot_fill
|
||||
|
||||
|
@ -1459,11 +1497,15 @@ def normalize_slot_fills(
|
|||
used_slot_name = content.slot_name or slot_name
|
||||
used_nodelist = content.nodelist
|
||||
used_contents = content.contents if content.contents is not None else content_func
|
||||
used_source = content.source
|
||||
used_extra = content.extra.copy()
|
||||
else:
|
||||
used_component_name = component_name
|
||||
used_slot_name = slot_name
|
||||
used_nodelist = None
|
||||
used_contents = content_func
|
||||
used_source = "python"
|
||||
used_extra = {}
|
||||
|
||||
slot = Slot(
|
||||
contents=used_contents,
|
||||
|
@ -1471,6 +1513,8 @@ def normalize_slot_fills(
|
|||
component_name=used_component_name,
|
||||
slot_name=used_slot_name,
|
||||
nodelist=used_nodelist,
|
||||
source=used_source,
|
||||
extra=used_extra,
|
||||
)
|
||||
|
||||
return slot
|
||||
|
@ -1500,6 +1544,8 @@ def _nodelist_to_slot(
|
|||
data_var: Optional[str] = None,
|
||||
fallback_var: Optional[str] = None,
|
||||
extra_context: Optional[Dict[str, Any]] = None,
|
||||
source: Optional[Literal["template", "python"]] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Slot:
|
||||
if data_var:
|
||||
if not data_var.isidentifier():
|
||||
|
@ -1591,7 +1637,9 @@ def _nodelist_to_slot(
|
|||
# `BaseNode.contents` which is `None` for self-closing tags like `{% fill "footer" / %}`.
|
||||
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
|
||||
# So we need to special-case this.
|
||||
contents=contents if contents is not None else "",
|
||||
contents=default(contents, ""),
|
||||
source=default(source, "python"),
|
||||
extra=default(extra, {}),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -210,7 +210,7 @@ class TestSlot:
|
|||
|
||||
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
||||
# See https://github.com/django-components/django-components/issues/1164#issuecomment-2854682354
|
||||
def test_slots_reuse_functions__string(self):
|
||||
def test_slots_same_contents__string(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
|
@ -229,13 +229,9 @@ class TestSlot:
|
|||
)
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.content_func is not None
|
||||
assert first_slot_func.contents == "FIRST_SLOT"
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "FIRST_SLOT"
|
||||
|
||||
captured_slots = {}
|
||||
SimpleComponent.render(
|
||||
|
@ -243,19 +239,15 @@ class TestSlot:
|
|||
)
|
||||
|
||||
second_slot_func = captured_slots["first"]
|
||||
second_nodelist: NodeList = second_slot_func.nodelist
|
||||
assert isinstance(second_slot_func, Slot)
|
||||
assert second_slot_func.content_func is not None
|
||||
assert second_slot_func.contents == "FIRST_SLOT"
|
||||
assert len(second_nodelist) == 1
|
||||
assert isinstance(second_nodelist[0], TextNode)
|
||||
assert second_nodelist[0].s == "FIRST_SLOT"
|
||||
|
||||
assert first_slot_func.contents == second_slot_func.contents
|
||||
|
||||
# Part of the slot caching feature - test that consistent functions passed as slots
|
||||
# reuse the slot function.
|
||||
def test_slots_reuse_functions__func(self):
|
||||
def test_slots_same_contents__func(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
|
@ -279,7 +271,6 @@ class TestSlot:
|
|||
assert isinstance(first_slot_func, Slot)
|
||||
assert callable(first_slot_func.content_func)
|
||||
assert callable(first_slot_func.contents)
|
||||
assert first_slot_func.nodelist is None
|
||||
|
||||
captured_slots = {}
|
||||
SimpleComponent.render(
|
||||
|
@ -290,13 +281,12 @@ class TestSlot:
|
|||
assert isinstance(second_slot_func, Slot)
|
||||
assert callable(second_slot_func.content_func)
|
||||
assert callable(second_slot_func.contents)
|
||||
assert second_slot_func.nodelist is None
|
||||
|
||||
assert first_slot_func.contents is second_slot_func.contents
|
||||
|
||||
# Part of the slot caching feature - test that `Slot` instances with identical function
|
||||
# passed as slots reuse the slot function.
|
||||
def test_slots_reuse_functions__slot(self):
|
||||
def test_slots_same_contents__slot(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
|
@ -320,7 +310,6 @@ class TestSlot:
|
|||
assert isinstance(first_slot_func, Slot)
|
||||
assert callable(first_slot_func.content_func)
|
||||
assert callable(first_slot_func.contents)
|
||||
assert first_slot_func.nodelist is None
|
||||
|
||||
captured_slots = {}
|
||||
SimpleComponent.render(
|
||||
|
@ -331,13 +320,12 @@ class TestSlot:
|
|||
assert isinstance(second_slot_func, Slot)
|
||||
assert callable(second_slot_func.content_func)
|
||||
assert callable(second_slot_func.contents)
|
||||
assert second_slot_func.nodelist is None
|
||||
|
||||
assert first_slot_func.contents == second_slot_func.contents
|
||||
|
||||
# Part of the slot caching feature - test that identical slot fill content
|
||||
# slots reuse the slot function.
|
||||
def test_slots_reuse_functions__fill_tag_default(self):
|
||||
def test_slots_same_contents__fill_tag_default(self):
|
||||
captured_slots = {}
|
||||
|
||||
@register("test")
|
||||
|
@ -363,31 +351,23 @@ class TestSlot:
|
|||
template.render(Context())
|
||||
|
||||
first_slot_func = captured_slots["default"]
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert callable(first_slot_func.content_func)
|
||||
assert first_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||
|
||||
captured_slots = {}
|
||||
template.render(Context())
|
||||
|
||||
second_slot_func = captured_slots["default"]
|
||||
second_nodelist: NodeList = second_slot_func.nodelist
|
||||
assert isinstance(second_slot_func, Slot)
|
||||
assert callable(second_slot_func.content_func)
|
||||
assert second_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||
assert len(second_nodelist) == 1
|
||||
assert isinstance(second_nodelist[0], TextNode)
|
||||
assert second_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||
|
||||
assert first_slot_func.contents == second_slot_func.contents
|
||||
|
||||
# Part of the slot caching feature - test that identical slot fill content
|
||||
# slots reuse the slot function.
|
||||
def test_slots_reuse_functions__fill_tag_named(self):
|
||||
def test_slots_same_contents__fill_tag_named(self):
|
||||
captured_slots = {}
|
||||
|
||||
@register("test")
|
||||
|
@ -415,28 +395,231 @@ class TestSlot:
|
|||
template.render(Context())
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert callable(first_slot_func.content_func)
|
||||
assert first_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||
|
||||
captured_slots = {}
|
||||
template.render(Context())
|
||||
|
||||
second_slot_func = captured_slots["first"]
|
||||
second_nodelist: NodeList = second_slot_func.nodelist
|
||||
assert isinstance(second_slot_func, Slot)
|
||||
assert callable(second_slot_func.content_func)
|
||||
assert second_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||
assert len(second_nodelist) == 1
|
||||
assert isinstance(second_nodelist[0], TextNode)
|
||||
assert second_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||
|
||||
assert first_slot_func.contents == second_slot_func.contents
|
||||
|
||||
def test_slot_metadata__string(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" required %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
SimpleComponent.render(
|
||||
slots={"first": "FIRST_SLOT"},
|
||||
)
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "SimpleComponent"
|
||||
assert first_slot_func.slot_name == "first"
|
||||
assert first_slot_func.source == "python"
|
||||
assert first_slot_func.extra == {}
|
||||
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "FIRST_SLOT"
|
||||
|
||||
# Part of the slot caching feature - test that consistent functions passed as slots
|
||||
# reuse the slot function.
|
||||
def test_slot_metadata__func(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" required %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||
|
||||
SimpleComponent.render(
|
||||
slots={"first": slot_func},
|
||||
)
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "SimpleComponent"
|
||||
assert first_slot_func.slot_name == "first"
|
||||
assert first_slot_func.source == "python"
|
||||
assert first_slot_func.extra == {}
|
||||
assert first_slot_func.nodelist is None
|
||||
|
||||
# Part of the slot caching feature - test that `Slot` instances with identical function
|
||||
# passed as slots reuse the slot function.
|
||||
def test_slot_metadata__slot(self):
|
||||
captured_slots = {}
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" required %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||
|
||||
SimpleComponent.render(
|
||||
slots={"first": Slot(slot_func, extra={"foo": "bar"}, slot_name="whoop")},
|
||||
)
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "SimpleComponent"
|
||||
assert first_slot_func.slot_name == "whoop"
|
||||
assert first_slot_func.source == "python"
|
||||
assert first_slot_func.extra == {"foo": "bar"}
|
||||
assert first_slot_func.nodelist is None
|
||||
|
||||
# Part of the slot caching feature - test that identical slot fill content
|
||||
# slots reuse the slot function.
|
||||
def test_slot_metadata__fill_tag_default(self):
|
||||
captured_slots = {}
|
||||
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" default %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
FROM_INSIDE_DEFAULT_SLOT
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
template.render(Context())
|
||||
|
||||
first_slot_func = captured_slots["default"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "test"
|
||||
assert first_slot_func.slot_name == "default"
|
||||
assert first_slot_func.source == "template"
|
||||
assert first_slot_func.extra == {}
|
||||
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||
|
||||
# Part of the slot caching feature - test that identical slot fill content
|
||||
# slots reuse the slot function.
|
||||
def test_slot_metadata__fill_tag_named(self):
|
||||
captured_slots = {}
|
||||
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" default %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill "first" %}
|
||||
FROM_INSIDE_NAMED_SLOT
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
template.render(Context())
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "test"
|
||||
assert first_slot_func.slot_name == "first"
|
||||
assert first_slot_func.source == "template"
|
||||
assert first_slot_func.extra == {}
|
||||
|
||||
first_nodelist: NodeList = first_slot_func.nodelist
|
||||
assert len(first_nodelist) == 1
|
||||
assert isinstance(first_nodelist[0], TextNode)
|
||||
assert first_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||
|
||||
# Part of the slot caching feature - test that identical slot fill content
|
||||
# slots reuse the slot function.
|
||||
def test_slot_metadata__fill_tag_body(self):
|
||||
captured_slots = {}
|
||||
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "first" default %}
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured_slots
|
||||
captured_slots = slots
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill "first" body=my_slot / %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
template.render(
|
||||
Context(
|
||||
{
|
||||
"my_slot": Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT", extra={"foo": "bar"}, slot_name="whoop"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
first_slot_func = captured_slots["first"]
|
||||
assert isinstance(first_slot_func, Slot)
|
||||
assert first_slot_func.component_name == "test"
|
||||
assert first_slot_func.slot_name == "whoop"
|
||||
assert first_slot_func.source == "template"
|
||||
assert first_slot_func.extra == {"foo": "bar"}
|
||||
assert first_slot_func.nodelist is None
|
||||
|
||||
def test_pass_body_to_fill__slot(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue