refactor: Rename {% fill default=... %} to {% fill fallback=... %} (#1190)

This commit is contained in:
Juro Oravec 2025-05-19 19:05:39 +02:00 committed by GitHub
parent 0d05ef4cb2
commit b6b574d875
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 167 additions and 91 deletions

View file

@ -322,10 +322,30 @@
ctx.template_data["my_template_var"] = "my_value" ctx.template_data["my_template_var"] = "my_value"
``` ```
**Miscellaneous** **Slots**
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1. - `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
- `SlotRef` was renamed to `SlotFallback`. The old name is deprecated and will be removed in v1.
- The `default` kwarg in `{% fill %}` tag was renamed to `fallback`. The old name is deprecated and will be removed in v1.
Before:
```django
{% fill "footer" default="footer" %}
{{ footer }}
{% endfill %}
```
After:
```django
{% fill "footer" fallback="footer" %}
{{ footer }}
{% endfill %}
```
#### Feat #### Feat
- New method to render template variables - `get_template_data()` - New method to render template variables - `get_template_data()`

View file

@ -50,7 +50,7 @@ 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, SlotFunc, SlotInput, SlotRef, SlotResult from django_components.slots import Slot, SlotContent, SlotFallback, SlotFunc, SlotInput, SlotRef, SlotResult
from django_components.tag_formatter import ( from django_components.tag_formatter import (
ComponentFormatter, ComponentFormatter,
ShorthandComponentFormatter, ShorthandComponentFormatter,
@ -125,6 +125,7 @@ __all__ = [
"ShorthandComponentFormatter", "ShorthandComponentFormatter",
"Slot", "Slot",
"SlotContent", "SlotContent",
"SlotFallback",
"SlotFunc", "SlotFunc",
"SlotInput", "SlotInput",
"SlotRef", "SlotRef",

View file

@ -67,11 +67,11 @@ from django_components.perfutil.provide import register_provide_reference, unreg
from django_components.provide import get_injected_context_var from django_components.provide import get_injected_context_var
from django_components.slots import ( from django_components.slots import (
Slot, Slot,
SlotFallback,
SlotFunc, SlotFunc,
SlotInput, SlotInput,
SlotIsFilled, SlotIsFilled,
SlotName, SlotName,
SlotRef,
SlotResult, SlotResult,
_is_extracting_fill, _is_extracting_fill,
resolve_fills, resolve_fills,
@ -2723,8 +2723,8 @@ class Component(metaclass=ComponentMeta):
# 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, slot_ref: SlotRef) -> SlotResult: def content_fn(ctx: Context, slot_data: Dict, fallback: SlotFallback) -> SlotResult:
rendered = content(ctx, slot_data, slot_ref) rendered = content(ctx, slot_data, fallback)
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)

View file

@ -45,7 +45,7 @@ SLOT_NAME_KWARG = "name"
SLOT_REQUIRED_FLAG = "required" SLOT_REQUIRED_FLAG = "required"
SLOT_DEFAULT_FLAG = "default" SLOT_DEFAULT_FLAG = "default"
FILL_DATA_KWARG = "data" FILL_DATA_KWARG = "data"
FILL_DEFAULT_KWARG = "default" FILL_FALLBACK_KWARG = "fallback"
# Public types # Public types
@ -54,7 +54,7 @@ SlotResult = Union[str, SafeString]
@runtime_checkable @runtime_checkable
class SlotFunc(Protocol, Generic[TSlotData]): class SlotFunc(Protocol, Generic[TSlotData]):
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotRef") -> SlotResult: ... # noqa E704 def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult: ... # noqa E704
@dataclass @dataclass
@ -102,7 +102,7 @@ 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: "SlotRef") -> SlotResult: def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult:
return self.content_func(ctx, slot_data, slot_ref) return self.content_func(ctx, slot_data, slot_ref)
# 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
@ -125,7 +125,7 @@ class Slot(Generic[TSlotData]):
nodelist=NodeList([TextNode(contents)]), nodelist=NodeList([TextNode(contents)]),
contents=contents, contents=contents,
data_var=None, data_var=None,
default_var=None, fallback_var=None,
) )
return slot.contents, slot.nodelist, slot.content_func return slot.contents, slot.nodelist, slot.content_func
@ -191,25 +191,36 @@ class SlotFill(Generic[TSlotData]):
slot: Slot[TSlotData] slot: Slot[TSlotData]
class SlotRef: class SlotFallback:
""" """
SlotRef allows to treat a slot as a variable. The slot is rendered only once SlotFallback allows to treat a slot fallback as a variable. The slot is rendered only once
the instance is coerced to string. the instance is coerced to string.
This is used to access slots as variables inside the templates. When a SlotRef This is used to access slots as variables inside the templates. When a `SlotFallback`
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
of the slot. of the slot.
Usage in slot functions:
```py
def slot_function(self, ctx: Context, slot_data: TSlotData, fallback: SlotFallback):
return f"Hello, {fallback}!"
```
""" """
def __init__(self, slot: "SlotNode", context: Context): def __init__(self, slot: "SlotNode", context: Context):
self._slot = slot self._slot = slot
self._context = context self._context = context
# Render the slot when the template coerces SlotRef to string # Render the slot when the template coerces SlotFallback to string
def __str__(self) -> str: def __str__(self) -> str:
return mark_safe(self._slot.nodelist.render(self._context)) return mark_safe(self._slot.nodelist.render(self._context))
# TODO_v1 - REMOVE - superseded by SlotFallback
SlotRef = SlotFallback
class SlotIsFilled(dict): class SlotIsFilled(dict):
""" """
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise. Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
@ -312,13 +323,13 @@ class SlotNode(BaseNode):
\"\"\" \"\"\"
``` ```
### Accessing default slot content ### Accessing fallback slot content
The content between the `{% slot %}..{% endslot %}` tags is the default content that The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
will be rendered if no fill is given for the slot. will be rendered if no fill is given for the slot.
This default content can then be accessed from within the [`{% fill %}`](#fill) tag using This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
the fill's `default` kwarg. the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content. This is useful if you need to wrap / prepend / append the original slot's content.
```python ```python
@ -327,7 +338,7 @@ class SlotNode(BaseNode):
template = \"\"\" template = \"\"\"
<div> <div>
{% slot "content" %} {% slot "content" %}
This is default content! This is fallback content!
{% endslot %} {% endslot %}
</div> </div>
\"\"\" \"\"\"
@ -337,10 +348,10 @@ class SlotNode(BaseNode):
@register("parent") @register("parent")
class Parent(Component): class Parent(Component):
template = \"\"\" template = \"\"\"
{# Parent can access the slot's default content #} {# Parent can access the slot's fallback content #}
{% component "child" %} {% component "child" %}
{% fill "content" default="default" %} {% fill "content" fallback="fallback" %}
{{ default }} {{ fallback }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
\"\"\" \"\"\"
@ -547,7 +558,7 @@ class SlotNode(BaseNode):
slot=slot_fill_fn, slot=slot_fill_fn,
) )
else: else:
# No fill was supplied, render the slot's default content # No fill was supplied, render the slot's fallback content
slot_fill = SlotFill( slot_fill = SlotFill(
name=slot_name, name=slot_name,
is_filled=False, is_filled=False,
@ -557,7 +568,7 @@ class SlotNode(BaseNode):
nodelist=self.nodelist, nodelist=self.nodelist,
contents=self.contents, contents=self.contents,
data_var=None, data_var=None,
default_var=None, fallback_var=None,
# Escaped because this was defined in the template # Escaped because this was defined in the template
escaped=True, escaped=True,
), ),
@ -625,7 +636,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 = SlotRef(self, context) slot_ref = 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)
@ -644,7 +655,7 @@ class SlotNode(BaseNode):
with used_ctx.render_context.push(render_ctx_layer): with used_ctx.render_context.push(render_ctx_layer):
with add_slot_to_error_message(component_name, slot_name): with add_slot_to_error_message(component_name, slot_name):
# Render slot as a function # Render slot as a function
# NOTE: While `{% fill %}` tag has to opt in for the `default` 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_fill.slot(used_ctx, kwargs, slot_ref) output = slot_fill.slot(used_ctx, kwargs, slot_ref)
@ -669,7 +680,7 @@ class SlotNode(BaseNode):
component_ctx: "ComponentContext", component_ctx: "ComponentContext",
) -> Context: ) -> Context:
"""Prepare the context used in a slot fill based on the settings.""" """Prepare the context used in a slot fill based on the settings."""
# If slot is NOT filled, we use the slot's default AKA content between # If slot is NOT filled, we use the slot's fallback AKA content between
# the `{% slot %}` tags. These should be evaluated as if the `{% slot %}` # the `{% slot %}` tags. These should be evaluated as if the `{% slot %}`
# tags weren't even there, which means that we use the current context. # tags weren't even there, which means that we use the current context.
if not slot_fill.is_filled: if not slot_fill.is_filled:
@ -696,11 +707,10 @@ class FillNode(BaseNode):
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for - `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
the default slot. the default slot.
- `default` (str, optional): This argument allows you to access the original content of the slot - `fallback` (str, optional): This argument allows you to access the original content of the slot
under the specified variable name. See under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
[Accessing original content of slots](../../concepts/fundamentals/slots#accessing-original-content-of-slots)
- `data` (str, optional): This argument allows you to access the data passed to the slot - `data` (str, optional): This argument allows you to access the data passed to the slot
under the specified variable name. See [Scoped slots](../../concepts/fundamentals/slots#scoped-slots) under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
**Examples:** **Examples:**
@ -713,7 +723,7 @@ class FillNode(BaseNode):
{% endcomponent %} {% endcomponent %}
``` ```
### Accessing slot's default content with the `default` kwarg ### Accessing slot's fallback content with the `fallback` kwarg
```django ```django
{# my_table.html #} {# my_table.html #}
@ -727,9 +737,9 @@ class FillNode(BaseNode):
```django ```django
{% component "my_table" %} {% component "my_table" %}
{% fill "pagination" default="default_pag" %} {% fill "pagination" fallback="fallback" %}
<div class="my-class"> <div class="my-class">
{{ default_pag }} {{ fallback }}
</div> </div>
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
@ -759,16 +769,16 @@ class FillNode(BaseNode):
{% endcomponent %} {% endcomponent %}
``` ```
### Accessing slot data and default content on the default slot ### Accessing slot data and fallback content on the default slot
To access slot data and the default slot content on the default slot, To access slot data and the fallback slot content on the default slot,
use `{% fill %}` with `name` set to `"default"`: use `{% fill %}` with `name` set to `"default"`:
```django ```django
{% component "button" %} {% component "button" %}
{% fill name="default" data="slot_data" default="default_slot" %} {% fill name="default" data="slot_data" fallback="slot_fallback" %}
You clicked me {{ slot_data.count }} times! You clicked me {{ slot_data.count }} times!
{{ default_slot }} {{ slot_fallback }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
``` ```
@ -778,7 +788,25 @@ class FillNode(BaseNode):
end_tag = "endfill" end_tag = "endfill"
allowed_flags = [] allowed_flags = []
def render(self, context: Context, name: str, *, data: Optional[str] = None, default: Optional[str] = None) -> str: def render(
self,
context: Context,
name: str,
*,
data: Optional[str] = None,
fallback: Optional[str] = None,
# TODO_V1: Use `fallback` kwarg instead of `default`
default: Optional[str] = None,
) -> str:
# TODO_V1: Use `fallback` kwarg instead of `default`
if fallback is not None and default is not None:
raise TemplateSyntaxError(
f"Fill tag received both 'default' and '{FILL_FALLBACK_KWARG}' kwargs. "
f"Use '{FILL_FALLBACK_KWARG}' instead."
)
elif fallback is None and default is not None:
fallback = default
if not _is_extracting_fill(context): if not _is_extracting_fill(context):
raise TemplateSyntaxError( raise TemplateSyntaxError(
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. " "FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
@ -797,28 +825,28 @@ class FillNode(BaseNode):
f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'" f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
) )
if default is not None: if fallback is not None:
if not isinstance(default, str): if not isinstance(fallback, str):
raise TemplateSyntaxError( raise TemplateSyntaxError(
f"Fill tag '{FILL_DEFAULT_KWARG}' kwarg must resolve to a string, got {default}" f"Fill tag '{FILL_FALLBACK_KWARG}' kwarg must resolve to a string, got {fallback}"
) )
if not is_identifier(default): if not is_identifier(fallback):
raise RuntimeError( raise RuntimeError(
f"Fill tag kwarg '{FILL_DEFAULT_KWARG}' does not resolve to a valid Python identifier," f"Fill tag kwarg '{FILL_FALLBACK_KWARG}' does not resolve to a valid Python identifier,"
f" got '{default}'" f" got '{fallback}'"
) )
# data and default cannot be bound to the same variable # data and fallback cannot be bound to the same variable
if data and default and data == default: if data and fallback and data == fallback:
raise RuntimeError( raise RuntimeError(
f"Fill '{name}' received the same string for slot default ({FILL_DEFAULT_KWARG}=...)" f"Fill '{name}' received the same string for slot fallback ({FILL_FALLBACK_KWARG}=...)"
f" and slot data ({FILL_DATA_KWARG}=...)" f" and slot data ({FILL_DATA_KWARG}=...)"
) )
fill_data = FillWithData( fill_data = FillWithData(
fill=self, fill=self,
name=name, name=name,
default_var=default, fallback_var=fallback,
data_var=data, data_var=data,
extra_context={}, extra_context={},
) )
@ -905,7 +933,7 @@ class FillNode(BaseNode):
class FillWithData(NamedTuple): class FillWithData(NamedTuple):
fill: FillNode fill: FillNode
name: str name: str
default_var: Optional[str] fallback_var: Optional[str]
data_var: Optional[str] data_var: Optional[str]
extra_context: Dict[str, Any] extra_context: Dict[str, Any]
@ -989,7 +1017,7 @@ def resolve_fills(
nodelist=nodelist, nodelist=nodelist,
contents=contents, contents=contents,
data_var=None, data_var=None,
default_var=None, fallback_var=None,
# Escaped because this was defined in the template # Escaped because this was defined in the template
escaped=True, escaped=True,
) )
@ -1005,7 +1033,7 @@ def resolve_fills(
nodelist=fill.fill.nodelist, nodelist=fill.fill.nodelist,
contents=fill.fill.contents, contents=fill.fill.contents,
data_var=fill.data_var, data_var=fill.data_var,
default_var=fill.default_var, fallback_var=fill.fallback_var,
extra_context=fill.extra_context, extra_context=fill.extra_context,
# Escaped because this was defined in the template # Escaped because this was defined in the template
escaped=True, escaped=True,
@ -1080,7 +1108,7 @@ def _nodelist_to_slot(
nodelist: NodeList, nodelist: NodeList,
contents: Optional[str] = None, contents: Optional[str] = None,
data_var: Optional[str] = None, data_var: Optional[str] = None,
default_var: Optional[str] = None, fallback_var: Optional[str] = None,
escaped: bool = False, escaped: bool = False,
extra_context: Optional[Dict[str, Any]] = None, extra_context: Optional[Dict[str, Any]] = None,
) -> Slot: ) -> Slot:
@ -1090,10 +1118,10 @@ def _nodelist_to_slot(
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'" f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'"
) )
if default_var: if fallback_var:
if not default_var.isidentifier(): if not fallback_var.isidentifier():
raise TemplateSyntaxError( raise TemplateSyntaxError(
f"Slot default alias in fill '{slot_name}' must be a valid identifier. Got '{default_var}'" f"Slot fallback alias in fill '{slot_name}' must be a valid identifier. Got '{fallback_var}'"
) )
# We use Template.render() to render the nodelist, so that Django correctly sets up # We use Template.render() to render the nodelist, so that Django correctly sets up
@ -1103,17 +1131,17 @@ 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: SlotRef) -> SlotResult: def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotFallback) -> SlotResult:
# 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 ctx[data_var] = slot_data
# If slot fill is using `{% fill "myslot" default="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 default slot from within the fill content. # the context, so users can refer to the fallback slot from within the fill content.
if default_var: if fallback_var:
ctx[default_var] = slot_ref ctx[fallback_var] = slot_ref
# 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

View file

@ -12,7 +12,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, SlotRef from django_components.slots import Slot, 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,7 +49,7 @@ class TestSlot:
"kwargs": kwargs, "kwargs": kwargs,
} }
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef): def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotFallback):
assert isinstance(ctx, Context) assert isinstance(ctx, 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
@ -72,7 +72,7 @@ class TestSlot:
} }
assert slot_data_expected == slot_data assert slot_data_expected == slot_data
assert isinstance(slot_ref, SlotRef) assert isinstance(slot_ref, SlotFallback)
assert "SLOT_DEFAULT" == str(slot_ref).strip() assert "SLOT_DEFAULT" == str(slot_ref).strip()
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}" return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"

View file

@ -895,8 +895,8 @@ class TestPassthroughSlots:
{% component "test" %} {% component "test" %}
{% if slot_names %} {% if slot_names %}
{% for slot in slot_names %} {% for slot in slot_names %}
{% fill name=slot default="default" %} {% fill name=slot fallback="fallback" %}
OVERRIDEN_SLOT "{{ slot }}" - INDEX {{ forloop.counter0 }} - ORIGINAL "{{ default }}" OVERRIDEN_SLOT "{{ slot }}" - INDEX {{ forloop.counter0 }} - ORIGINAL "{{ fallback }}"
{% endfill %} {% endfill %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -950,8 +950,8 @@ class TestPassthroughSlots:
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% with slot="header" %} {% with slot="header" %}
{% fill name=slot default="default" %} {% fill name=slot fallback="fallback" %}
OVERRIDEN_SLOT "{{ slot }}" - ORIGINAL "{{ default }}" OVERRIDEN_SLOT "{{ slot }}" - ORIGINAL "{{ fallback }}"
{% endfill %} {% endfill %}
{% endwith %} {% endwith %}
{% endcomponent %} {% endcomponent %}
@ -996,7 +996,7 @@ class TestPassthroughSlots:
{% if slot_names %} {% if slot_names %}
{% for slot in slot_names %} {% for slot in slot_names %}
{{ forloop.counter0 }} {{ forloop.counter0 }}
{% fill name=slot default="default" %} {% fill name=slot fallback="fallback" %}
OVERRIDEN_SLOT OVERRIDEN_SLOT
{% endfill %} {% endfill %}
{% endfor %} {% endfor %}
@ -1365,9 +1365,11 @@ class TestSlottedTemplateRegression:
@djc_test @djc_test
class TestSlotDefault: class TestSlotFallback:
# TODO_v1 - REMOVE
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_basic(self, components_settings): def test_basic_legacy(self, components_settings):
registry.register("test", _gen_slotted_component()) registry.register("test", _gen_slotted_component())
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
@ -1391,13 +1393,38 @@ class TestSlotDefault:
""", """,
) )
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_basic(self, components_settings):
registry.register("test", _gen_slotted_component())
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "header" fallback="header" %}Before: {{ header }}{% endfill %}
{% fill "main" fallback="main" %}{{ main }}{% endfill %}
{% fill "footer" fallback="footer" %}{{ footer }}, after{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assertHTMLEqual(
rendered,
"""
<custom-template data-djc-id-ca1bc42>
<header>Before: Default header</header>
<main>Default main</main>
<footer>Default footer, after</footer>
</custom-template>
""",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_multiple_calls(self, components_settings): def test_multiple_calls(self, components_settings):
registry.register("test", _gen_slotted_component()) registry.register("test", _gen_slotted_component())
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" default="header" %} {% fill "header" fallback="header" %}
First: {{ header }}; First: {{ header }};
Second: {{ header }} Second: {{ header }}
{% endfill %} {% endfill %}
@ -1423,7 +1450,7 @@ class TestSlotDefault:
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" default="header" %} {% fill "header" fallback="header" %}
{% for i in range %} {% for i in range %}
{% if forloop.first %} {% if forloop.first %}
First {{ header }} First {{ header }}
@ -1454,14 +1481,14 @@ class TestSlotDefault:
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" default="header1" %} {% fill "header" fallback="header1" %}
header1_in_header1: {{ header1 }} header1_in_header1: {{ header1 }}
{% component "test" %} {% component "test" %}
{% fill "header" default="header2" %} {% fill "header" fallback="header2" %}
header1_in_header2: {{ header1 }} header1_in_header2: {{ header1 }}
header2_in_header2: {{ header2 }} header2_in_header2: {{ header2 }}
{% endfill %} {% endfill %}
{% fill "footer" default="footer2" %} {% fill "footer" fallback="footer2" %}
header1_in_footer2: {{ header1 }} header1_in_footer2: {{ header1 }}
footer2_in_footer2: {{ footer2 }} footer2_in_footer2: {{ footer2 }}
{% endfill %} {% endfill %}
@ -1570,7 +1597,7 @@ class TestScopedSlot:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_data_with_slot_default(self, components_settings): def test_slot_data_with_slot_fallback(self, components_settings):
@register("test") @register("test")
class TestComponent(Component): class TestComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -1589,8 +1616,8 @@ class TestScopedSlot:
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" default="slot_var" %} {% fill "my_slot" data="slot_data_in_fill" fallback="fallback" %}
{{ slot_var }} {{ fallback }}
{{ slot_data_in_fill.abc }} {{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.var123 }} {{ slot_data_in_fill.var123 }}
{% endfill %} {% endfill %}
@ -1681,7 +1708,7 @@ class TestScopedSlot:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_data_and_default_on_default_slot(self, components_settings): def test_slot_data_and_fallback_on_default_slot(self, components_settings):
@register("test") @register("test")
class TestComponent(Component): class TestComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -1701,7 +1728,7 @@ class TestScopedSlot:
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill name="default" data="slot_data_in_fill" default="slot_var" %} {% fill name="default" data="slot_data_in_fill" fallback="slot_var" %}
{{ slot_data_in_fill.abc }} {{ slot_data_in_fill.abc }}
{{ slot_var }} {{ slot_var }}
{{ slot_data_in_fill.var123 }} {{ slot_data_in_fill.var123 }}
@ -1718,7 +1745,7 @@ class TestScopedSlot:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_data_raises_on_slot_data_and_slot_default_same_var(self, components_settings): def test_slot_data_raises_on_slot_data_and_slot_fallback_same_var(self, components_settings):
@register("test") @register("test")
class TestComponent(Component): class TestComponent(Component):
template: types.django_html = """ template: types.django_html = """
@ -1737,14 +1764,14 @@ class TestScopedSlot:
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "my_slot" data="slot_var" default="slot_var" %} {% fill "my_slot" data="slot_var" fallback="slot_var" %}
{{ slot_var }} {{ slot_var }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
with pytest.raises( with pytest.raises(
RuntimeError, RuntimeError,
match=re.escape("Fill 'my_slot' received the same string for slot default (default=...) and slot data (data=...)"), # noqa: E501 match=re.escape("Fill 'my_slot' received the same string for slot fallback (fallback=...) and slot data (data=...)"), # noqa: E501
): ):
Template(template).render(Context()) Template(template).render(Context())

View file

@ -445,7 +445,7 @@ class TestSlotIteration:
["django", "isolated"], ["django", "isolated"],
) )
) )
def test_inner_slot_iteration_nested_with_slot_default(self, components_settings, expected): def test_inner_slot_iteration_nested_with_slot_fallback(self, components_settings, expected):
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
objects = [ objects = [
@ -458,7 +458,7 @@ class TestSlotIteration:
{% component "slot_in_a_loop" objects=objects %} {% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" %} {% fill "slot_inner" %}
{% component "slot_in_a_loop" objects=object.inner %} {% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" default="super_slot_inner" %} {% fill "slot_inner" fallback="super_slot_inner" %}
{{ super_slot_inner }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
@ -498,7 +498,7 @@ class TestSlotIteration:
["django", "isolated"], ["django", "isolated"],
) )
) )
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable( def test_inner_slot_iteration_nested_with_slot_fallback_and_outer_scope_variable(
self, self,
components_settings, components_settings,
expected, expected,
@ -516,7 +516,7 @@ class TestSlotIteration:
{% fill "slot_inner" %} {% fill "slot_inner" %}
{{ outer_scope_variable_1 }} {{ outer_scope_variable_1 }}
{% component "slot_in_a_loop" objects=object.inner %} {% component "slot_in_a_loop" objects=object.inner %}
{% fill "slot_inner" default="super_slot_inner" %} {% fill "slot_inner" fallback="super_slot_inner" %}
{{ outer_scope_variable_2 }} {{ outer_scope_variable_2 }}
{{ super_slot_inner }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
@ -537,7 +537,7 @@ class TestSlotIteration:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(components_settings={"context_behavior": "isolated"}) @djc_test(components_settings={"context_behavior": "isolated"})
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable__isolated_2( def test_inner_slot_iteration_nested_with_slot_fallback_and_outer_scope_variable__isolated_2(
self, self,
): ):
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
@ -556,7 +556,7 @@ class TestSlotIteration:
{% fill "slot_inner" %} {% fill "slot_inner" %}
{{ outer_scope_variable_1|safe }} {{ outer_scope_variable_1|safe }}
{% component "slot_in_a_loop" objects=objects %} {% component "slot_in_a_loop" objects=objects %}
{% fill "slot_inner" default="super_slot_inner" %} {% fill "slot_inner" fallback="super_slot_inner" %}
{{ outer_scope_variable_2|safe }} {{ outer_scope_variable_2|safe }}
{{ super_slot_inner }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
@ -959,14 +959,14 @@ class TestComponentNesting:
["django", "isolated"], ["django", "isolated"],
) )
) )
def test_component_nesting_component_with_slot_default(self, components_settings, expected): def test_component_nesting_component_with_slot_fallback(self, components_settings, expected):
registry.register("dashboard", self._get_dashboard_component()) registry.register("dashboard", self._get_dashboard_component())
registry.register("calendar", self._get_calendar_component()) registry.register("calendar", self._get_calendar_component())
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %} {% component "dashboard" %}
{% fill "header" default="h" %} Hello! {{ h }} {% endfill %} {% fill "header" fallback="h" %} Hello! {{ h }} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)