refactor: change fill alias from "as var" to default=var (#504)

This commit is contained in:
Juro Oravec 2024-05-23 20:25:16 +02:00 committed by GitHub
parent c07f0e6341
commit edb2f347f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 238 additions and 111 deletions

View file

@ -44,6 +44,12 @@ Read on to learn about the details!
## Release notes ## Release notes
🚨📢 **Version 0.77** CHANGED the syntax for accessing default slot content.
- Previously, the syntax was
`{% fill "my_slot" as "alias" %}` and `{{ alias.default }}`.
- Now, the syntax is
`{% fill "my_slot" default="alias" %}` and `{{ alias }}`.
**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components. **Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components.
🚨📢 **Version 0.70** 🚨📢 **Version 0.70**
@ -627,13 +633,26 @@ Which you can then use are regular default slot:
_Added in version 0.26_ _Added in version 0.26_
Certain properties of a slot can be accessed from within a 'fill' context. They are provided as attributes on a user-defined alias of the targeted slot. For instance, let's say you're filling a slot called 'body'. To access properties of this slot, alias it using the 'as' keyword to a new name -- or keep the original name. With the new slot alias, you can call `<alias>.default` to insert the default content. > NOTE: In version 0.77, the syntax was changed from
> ```django
> {% fill "my_slot" as "alias" %} {{ alias.default }}
> ```
> to
> ```django
> {% fill "my_slot" default="slot_default" %} {{ slot_default }}
> ```
Notice the use of `as "body"` below: Sometimes you may want to keep the original slot, but only wrap or prepend/append content to it. To do so, you can access the default slot via the `default` kwarg.
Similarly to the `data` attribute, you specify the variable name through which the default slot will be made available.
For instance, let's say you're filling a slot called 'body'. To render the original slot, assign it to a variable using the `'default'` keyword. You then render this variable to insert the default content:
```htmldjango ```htmldjango
{% component "calendar" date="2020-06-06" %} {% component "calendar" date="2020-06-06" %}
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %} {% fill "body" default="body_default" %}
{{ body_default }}. Have a great day!
{% endfill %}
{% endcomponent %} {% endcomponent %}
``` ```
@ -832,12 +851,12 @@ While this does not:
``` ```
Note: You cannot set the `data` attribute and Note: You cannot set the `data` attribute and
[slot alias (`as var` syntax)](#accessing-original-content-of-slots) [`default` attribute)](#accessing-original-content-of-slots)
to the same name. This raises an error: to the same name. This raises an error:
```django ```django
{% component "my_comp" %} {% component "my_comp" %}
{% fill "content" data="slot_var" as "slot_var" %} {% fill "content" data="slot_var" default="slot_var" %}
{{ slot_var.input }} {{ slot_var.input }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}

View file

@ -358,8 +358,8 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
slot_fills = { slot_fills = {
slot_name: FillContent( slot_name: FillContent(
nodes=NodeList([TextNode(escape(content) if escape_content else content)]), nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
alias=None, slot_default_var=None,
scope=None, slot_data_var=None,
) )
for (slot_name, content) in slots_data.items() for (slot_name, content) in slots_data.items()
} }
@ -422,12 +422,12 @@ class ComponentNode(Node):
f"Detected duplicate fill tag name '{resolved_name}'." f"Detected duplicate fill tag name '{resolved_name}'."
) )
resolved_fill_alias = fill_node.resolve_alias(context, resolved_component_name) resolved_slot_default_var = fill_node.resolve_slot_default(context, resolved_component_name)
resolved_scope_var = fill_node.resolve_scope(context, resolved_component_name) resolved_slot_data_var = fill_node.resolve_slot_data(context, resolved_component_name)
fill_content[resolved_name] = FillContent( fill_content[resolved_name] = FillContent(
nodes=fill_node.nodelist, nodes=fill_node.nodelist,
alias=resolved_fill_alias, slot_default_var=resolved_slot_default_var,
scope=resolved_scope_var, slot_data_var=resolved_slot_data_var,
) )
component: Component = component_cls( component: Component = component_cls(

View file

@ -24,8 +24,8 @@ DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
SlotId = str SlotId = str
SlotName = str SlotName = str
AliasName = str SlotDefaultName = str
ScopeName = str SlotDataName = str
class FillContent(NamedTuple): class FillContent(NamedTuple):
@ -44,8 +44,8 @@ class FillContent(NamedTuple):
""" """
nodes: NodeList nodes: NodeList
alias: Optional[AliasName] slot_default_var: Optional[SlotDefaultName]
scope: Optional[ScopeName] slot_data_var: Optional[SlotDataName]
class Slot(NamedTuple): class Slot(NamedTuple):
@ -80,27 +80,26 @@ class SlotFill(NamedTuple):
is_filled: bool is_filled: bool
nodelist: NodeList nodelist: NodeList
context_data: Dict context_data: Dict
alias: Optional[AliasName] slot_default_var: Optional[SlotDefaultName]
scope: Optional[ScopeName] slot_data_var: Optional[SlotDataName]
class UserSlotVar: class LazySlot:
""" """
Extensible mechanism for offering 'fill' blocks in template access to properties Bound a slot and a context, so we can move these around as a single variable,
of parent slot. and lazily render the slot with given context only once coerced to string.
How it works: At render time, SlotNode(s) that have been aliased in the fill tag This is used to access slots as variables inside the templates. When a LazySlot
of the component instance create an instance of UserSlotVar. This instance is made is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
available to the rendering context on a key matching the slot alias (see of the slot.
SlotNode.render() for implementation).
""" """
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
@property # Render the slot when the template coerces LazySlot to string
def default(self) -> str: def __str__(self) -> str:
return mark_safe(self._slot.nodelist.render(self._context)) return mark_safe(self._slot.nodelist.render(self._context))
@ -141,20 +140,28 @@ class SlotNode(Node):
extra_context: Dict[str, Any] = {} extra_context: Dict[str, Any] = {}
# If slot is using alias `{% slot "myslot" as "abc" %}`, then set the "abc" to # If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
# the context, so users can refer to the slot from within the slot. # the context, so users can refer to the default slot from within the fill content.
if slot_fill.alias: default_var = slot_fill.slot_default_var
if not slot_fill.alias.isidentifier(): if default_var:
raise TemplateSyntaxError(f"Invalid fill alias. Must be a valid identifier. Got '{slot_fill.alias}'") if not default_var.isidentifier():
extra_context[slot_fill.alias] = UserSlotVar(self, context) raise TemplateSyntaxError(
f"Slot default alias in fill '{self.name}' must be a valid identifier. Got '{default_var}'"
)
extra_context[default_var] = LazySlot(self, context)
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs # Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
# are made available through a variable name that was set on the `{% fill %}` # are made available through a variable name that was set on the `{% fill %}`
# tag. # tag.
if slot_fill.scope: data_var = slot_fill.slot_data_var
if data_var:
if not data_var.isidentifier():
raise TemplateSyntaxError(
f"Slot data alias in fill '{self.name}' must be a valid identifier. Got '{data_var}'"
)
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context) slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
slot_kwargs = process_aggregate_kwargs(slot_kwargs) slot_kwargs = process_aggregate_kwargs(slot_kwargs)
extra_context[slot_fill.scope] = slot_kwargs extra_context[data_var] = slot_kwargs
# 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)
@ -192,17 +199,17 @@ class FillNode(Node):
self, self,
nodelist: NodeList, nodelist: NodeList,
name_fexp: FilterExpression, name_fexp: FilterExpression,
alias_fexp: Optional[FilterExpression] = None, slot_default_var_fexp: Optional[FilterExpression] = None,
scope_fexp: Optional[FilterExpression] = None, slot_data_var_fexp: Optional[FilterExpression] = None,
is_implicit: bool = False, is_implicit: bool = False,
node_id: Optional[str] = None, node_id: Optional[str] = None,
): ):
self.node_id = node_id or gen_id() self.node_id = node_id or gen_id()
self.nodelist = nodelist self.nodelist = nodelist
self.name_fexp = name_fexp self.name_fexp = name_fexp
self.alias_fexp = alias_fexp self.slot_default_var_fexp = slot_default_var_fexp
self.is_implicit = is_implicit self.is_implicit = is_implicit
self.scope_fexp = scope_fexp self.slot_data_var_fexp = slot_data_var_fexp
self.component_id: Optional[str] = None self.component_id: Optional[str] = None
def render(self, context: Context) -> str: def render(self, context: Context) -> str:
@ -215,11 +222,11 @@ class FillNode(Node):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>" return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
def resolve_alias(self, context: Context, component_name: Optional[str] = None) -> Optional[str]: def resolve_slot_default(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
return self.resolve_fexp("alias", self.alias_fexp, context, component_name) return self.resolve_fexp("slot default", self.slot_default_var_fexp, context, component_name)
def resolve_scope(self, context: Context, component_name: Optional[str] = None) -> Optional[str]: def resolve_slot_data(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
return self.resolve_fexp("scope", self.scope_fexp, context, component_name) return self.resolve_fexp("slot data", self.slot_data_var_fexp, context, component_name)
def resolve_fexp( def resolve_fexp(
self, self,
@ -232,14 +239,14 @@ class FillNode(Node):
return None return None
try: try:
resolved_alias = resolve_expression_as_identifier(context, fexp) resolved_name = resolve_expression_as_identifier(context, fexp)
except ValueError as err: except ValueError as err:
raise TemplateSyntaxError( raise TemplateSyntaxError(
f"Fill tag {name} '{fexp.var}' in component {component_name}" f"Fill tag {name} '{fexp.var}' in component {component_name}"
f"does not resolve to a valid Python identifier." f"does not resolve to a valid Python identifier."
) from err ) from err
return resolved_alias return resolved_name
def parse_slot_fill_nodes_from_component_nodelist( def parse_slot_fill_nodes_from_component_nodelist(
@ -363,8 +370,8 @@ def resolve_slots(
is_filled=True, is_filled=True,
nodelist=fill.nodes, nodelist=fill.nodes,
context_data=context_data, context_data=context_data,
alias=fill.alias, slot_default_var=fill.slot_default_var,
scope=fill.scope, slot_data_var=fill.slot_data_var,
) )
for name, fill in fill_content.items() for name, fill in fill_content.items()
} }
@ -452,8 +459,8 @@ def resolve_slots(
is_filled=False, is_filled=False,
nodelist=slot.nodelist, nodelist=slot.nodelist,
context_data=context_data, context_data=context_data,
alias=None, slot_default_var=None,
scope=None, slot_data_var=None,
) )
# Since the slot's default CAN include other slots (because it's defined in # Since the slot's default CAN include other slots (because it's defined in
# the same template), we need to enqueue the slot's children # the same template), we need to enqueue the slot's children
@ -500,8 +507,8 @@ def _resolve_default_slot(
is_filled=default_fill.is_filled, is_filled=default_fill.is_filled,
nodelist=default_fill.nodelist, nodelist=default_fill.nodelist,
context_data=default_fill.context_data, context_data=default_fill.context_data,
alias=default_fill.alias, slot_default_var=default_fill.slot_default_var,
scope=default_fill.scope, slot_data_var=default_fill.slot_data_var,
# Updated fields # Updated fields
name=slot.name, name=slot.name,
escaped_name=_escape_slot_name(slot.name), escaped_name=_escape_slot_name(slot.name),

View file

@ -31,6 +31,7 @@ register = django.template.Library()
SLOT_REQUIRED_OPTION_KEYWORD = "required" SLOT_REQUIRED_OPTION_KEYWORD = "required"
SLOT_DEFAULT_OPTION_KEYWORD = "default" SLOT_DEFAULT_OPTION_KEYWORD = "default"
SLOT_DATA_ATTR = "data" SLOT_DATA_ATTR = "data"
SLOT_DEFAULT_ATTR = "default"
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]: def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
@ -147,7 +148,7 @@ def do_fill(parser: Parser, token: Token) -> FillNode:
""" """
# e.g. {% fill <name> %} # e.g. {% fill <name> %}
tag_name, *args = token.split_contents() tag_name, *args = token.split_contents()
slot_name_fexp, alias_fexp, scope_var_fexp = _parse_fill_args(parser, args, tag_name) slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp = _parse_fill_args(parser, args, tag_name)
# Use a unique ID to be able to tie the fill nodes with components and slots # Use a unique ID to be able to tie the fill nodes with components and slots
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering # NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
@ -160,8 +161,8 @@ def do_fill(parser: Parser, token: Token) -> FillNode:
fill_node = FillNode( fill_node = FillNode(
nodelist, nodelist,
name_fexp=slot_name_fexp, name_fexp=slot_name_fexp,
alias_fexp=alias_fexp, slot_default_var_fexp=slot_default_var_fexp,
scope_fexp=scope_var_fexp, slot_data_var_fexp=slot_data_var_fexp,
node_id=fill_id, node_id=fill_id,
) )
@ -382,19 +383,13 @@ def _parse_fill_args(
) -> Tuple[FilterExpression, Optional[FilterExpression], Optional[FilterExpression]]: ) -> Tuple[FilterExpression, Optional[FilterExpression], Optional[FilterExpression]]:
if not len(bits): if not len(bits):
raise TemplateSyntaxError( raise TemplateSyntaxError(
"'fill' tag does not match pattern " f"{{% fill <name> [{SLOT_DATA_ATTR}=val] [as alias] %}}. " "'fill' tag does not match pattern "
f"{{% fill <name> [{SLOT_DATA_ATTR}=val] [{SLOT_DEFAULT_ATTR}=slot_var] %}} "
) )
slot_name = bits.pop(0) slot_name = bits.pop(0)
slot_name_fexp = parser.compile_filter(slot_name) slot_name_fexp = parser.compile_filter(slot_name)
alias_fexp: Optional[FilterExpression] = None
# e.g. {% fill <name> as <alias> %}
if len(bits) >= 2 and bits[-2].lower() == "as":
alias = bits.pop()
bits.pop() # Remove the "as" keyword
alias_fexp = parser.compile_filter(alias)
# Even tho we want to parse only single kwarg, we use the same logic for parsing # Even tho we want to parse only single kwarg, we use the same logic for parsing
# as we use for other tags, for consistency. # as we use for other tags, for consistency.
_, tag_kwarg_pairs = parse_bits( _, tag_kwarg_pairs = parse_bits(
@ -410,17 +405,29 @@ def _parse_fill_args(
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'") raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
tag_kwargs[key] = val tag_kwargs[key] = val
scope_var_fexp: Optional[FilterExpression] = None # Extract known kwargs
slot_data_var_fexp: Optional[FilterExpression] = None
if SLOT_DATA_ATTR in tag_kwargs: if SLOT_DATA_ATTR in tag_kwargs:
scope_var_fexp = tag_kwargs.pop(SLOT_DATA_ATTR) slot_data_var_fexp = tag_kwargs.pop(SLOT_DATA_ATTR)
if not is_wrapped_in_quotes(scope_var_fexp.token): if not is_wrapped_in_quotes(slot_data_var_fexp.token):
raise TemplateSyntaxError( raise TemplateSyntaxError(
f"Value of '{SLOT_DATA_ATTR}' in '{tag_name}' tag must be a string literal, got '{scope_var_fexp}'" f"Value of '{SLOT_DATA_ATTR}' in '{tag_name}' tag must be a string literal, got '{slot_data_var_fexp}'"
) )
if scope_var_fexp and alias_fexp and scope_var_fexp.token == alias_fexp.token: slot_default_var_fexp: Optional[FilterExpression] = None
if SLOT_DEFAULT_ATTR in tag_kwargs:
slot_default_var_fexp = tag_kwargs.pop(SLOT_DEFAULT_ATTR)
if not is_wrapped_in_quotes(slot_default_var_fexp.token):
raise TemplateSyntaxError( raise TemplateSyntaxError(
f"'{tag_name}' received the same string for slot alias (as ...) and slot data ({SLOT_DATA_ATTR}=...)" f"Value of '{SLOT_DEFAULT_ATTR}' in '{tag_name}' tag must be a string literal,"
f" got '{slot_default_var_fexp}'"
)
# data and default cannot be bound to the same variable
if slot_data_var_fexp and slot_default_var_fexp and slot_data_var_fexp.token == slot_default_var_fexp.token:
raise TemplateSyntaxError(
f"'{tag_name}' received the same string for slot default ({SLOT_DEFAULT_ATTR}=...)"
f" and slot data ({SLOT_DATA_ATTR}=...)"
) )
if len(tag_kwargs): if len(tag_kwargs):
@ -428,7 +435,7 @@ def _parse_fill_args(
extra_keys = ", ".join(extra_keywords) extra_keys = ", ".join(extra_keywords)
raise TemplateSyntaxError(f"'{tag_name}' received unexpected kwargs: {extra_keys}") raise TemplateSyntaxError(f"'{tag_name}' received unexpected kwargs: {extra_keys}")
return slot_name_fexp, alias_fexp, scope_var_fexp return slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp
def _get_positional_param( def _get_positional_param(

View file

@ -912,7 +912,7 @@ class ConditionalSlotTests(BaseTestCase):
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>') self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
class SlotSuperTests(BaseTestCase): class SlotDefaultTests(BaseTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
@ -924,13 +924,13 @@ class SlotSuperTests(BaseTestCase):
super().tearDownClass() super().tearDownClass()
component.registry.clear() component.registry.clear()
def test_basic_super_functionality(self): def test_basic(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" as "header" %}Before: {{ header.default }}{% endfill %} {% fill "header" default="header" %}Before: {{ header }}{% endfill %}
{% fill "main" as "main" %}{{ main.default }}{% endfill %} {% fill "main" default="main" %}{{ main }}{% endfill %}
{% fill "footer" as "footer" %}{{ footer.default }}, after{% endfill %} {% fill "footer" default="footer" %}{{ footer }}, after{% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
@ -947,13 +947,13 @@ class SlotSuperTests(BaseTestCase):
""", """,
) )
def test_multiple_super_calls(self): def test_multiple_calls(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" as "header" %} {% fill "header" default="header" %}
First: {{ header.default }}; First: {{ header }};
Second: {{ header.default }} Second: {{ header }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
@ -971,14 +971,16 @@ class SlotSuperTests(BaseTestCase):
""", """,
) )
def test_super_under_if_node(self): def test_under_if_and_forloop(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "header" as "header" %} {% fill "header" default="header" %}
{% for i in range %} {% for i in range %}
{% if forloop.first %}First {{ header.default }} {% if forloop.first %}
{% else %}Later {{ header.default }} First {{ header }}
{% else %}
Later {{ header }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfill %} {% endfill %}
@ -998,6 +1000,52 @@ class SlotSuperTests(BaseTestCase):
""", """,
) )
def test_nested_fills(self):
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "header" default="header1" %}
header1_in_header1: {{ header1 }}
{% component "test" %}
{% fill "header" default="header2" %}
header1_in_header2: {{ header1 }}
header2_in_header2: {{ header2 }}
{% endfill %}
{% fill "footer" default="footer2" %}
header1_in_footer2: {{ header1 }}
footer2_in_footer2: {{ footer2 }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>
header1_in_header1: Default header
<custom-template>
<header>
header1_in_header2: Default header
header2_in_header2: Default header
</header>
<main>Default main</main>
<footer>
header1_in_footer2: Default header
footer2_in_footer2: Default footer
</footer>
</custom-template>
</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""",
)
class TemplateSyntaxErrorTests(BaseTestCase): class TemplateSyntaxErrorTests(BaseTestCase):
@classmethod @classmethod
@ -1310,11 +1358,11 @@ class ComponentNestingTests(BaseTestCase):
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
@override_settings(COMPONENTS={"context_behavior": "django"}) @override_settings(COMPONENTS={"context_behavior": "django"})
def test_component_nesting_component_with_fill_and_super__django(self): def test_component_nesting_component_with_slot_default__django(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %} {% component "dashboard" %}
{% fill "header" as "h" %} Hello! {{ h.default }} {% endfill %} {% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
@ -1338,11 +1386,11 @@ class ComponentNestingTests(BaseTestCase):
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
@override_settings(COMPONENTS={"context_behavior": "isolated"}) @override_settings(COMPONENTS={"context_behavior": "isolated"})
def test_component_nesting_component_with_fill_and_super__isolated(self): def test_component_nesting_component_with_slot_default__isolated(self):
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "dashboard" %} {% component "dashboard" %}
{% fill "header" as "h" %} Hello! {{ h.default }} {% endfill %} {% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
template = Template(template_str) template = Template(template_str)
@ -2194,8 +2242,8 @@ class IterationFillTest(BaseTestCase):
{% 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" as "super_slot_inner" %} {% fill "slot_inner" default="super_slot_inner" %}
{{ super_slot_inner.default }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
{% endfill %} {% endfill %}
@ -2228,8 +2276,8 @@ class IterationFillTest(BaseTestCase):
{% 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" as "super_slot_inner" %} {% fill "slot_inner" default="super_slot_inner" %}
{{ super_slot_inner.default }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
{% endfill %} {% endfill %}
@ -2256,9 +2304,9 @@ class IterationFillTest(BaseTestCase):
{% 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" as "super_slot_inner" %} {% fill "slot_inner" default="super_slot_inner" %}
{{ outer_scope_variable_2 }} {{ outer_scope_variable_2 }}
{{ super_slot_inner.default }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
{% endfill %} {% endfill %}
@ -2311,9 +2359,9 @@ class IterationFillTest(BaseTestCase):
{% 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" as "super_slot_inner" %} {% fill "slot_inner" default="super_slot_inner" %}
{{ outer_scope_variable_2 }} {{ outer_scope_variable_2 }}
{{ super_slot_inner.default }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
{% endfill %} {% endfill %}
@ -2358,9 +2406,9 @@ class IterationFillTest(BaseTestCase):
{% 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" as "super_slot_inner" %} {% fill "slot_inner" default="super_slot_inner" %}
{{ outer_scope_variable_2|safe }} {{ outer_scope_variable_2|safe }}
{{ super_slot_inner.default }} {{ super_slot_inner }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
{% endfill %} {% endfill %}
@ -2401,7 +2449,7 @@ class ScopedSlotTest(BaseTestCase):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" abc=abc def=var123 %}Default text{% endslot %} {% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
</div> </div>
""" """
@ -2416,7 +2464,7 @@ class ScopedSlotTest(BaseTestCase):
{% component "test" %} {% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" %} {% fill "my_slot" data="slot_data_in_fill" %}
{{ slot_data_in_fill.abc }} {{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.def }} {{ slot_data_in_fill.var123 }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
@ -2435,7 +2483,7 @@ class ScopedSlotTest(BaseTestCase):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" default abc=abc 123=var123 required %}Default text{% endslot %} {% slot "my_slot" default abc=abc var123=var123 required %}Default text{% endslot %}
</div> </div>
""" """
@ -2450,7 +2498,7 @@ class ScopedSlotTest(BaseTestCase):
{% component "test" %} {% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" %} {% fill "my_slot" data="slot_data_in_fill" %}
{{ slot_data_in_fill.abc }} {{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.123 }} {{ slot_data_in_fill.var123 }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
@ -2463,13 +2511,13 @@ class ScopedSlotTest(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
def test_slot_data_fill_with_as(self): def test_slot_data_with_slot_default(self):
@component.register("test") @component.register("test")
class TestComponent(component.Component): class TestComponent(component.Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %} {% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
</div> </div>
""" """
@ -2482,10 +2530,10 @@ class ScopedSlotTest(BaseTestCase):
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" as "slot_var" %} {% fill "my_slot" data="slot_data_in_fill" default="slot_var" %}
{{ slot_var.default }} {{ slot_var }}
{{ slot_data_in_fill.abc }} {{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.123 }} {{ slot_data_in_fill.var123 }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
@ -2499,13 +2547,13 @@ class ScopedSlotTest(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
def test_slot_data_raises_on_slot_data_and_as_same_var(self): def test_slot_data_raises_on_slot_data_and_slot_default_same_var(self):
@component.register("test") @component.register("test")
class TestComponent(component.Component): class TestComponent(component.Component):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %} {% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
</div> </div>
""" """
@ -2518,14 +2566,14 @@ class ScopedSlotTest(BaseTestCase):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "test" %} {% component "test" %}
{% fill "my_slot" data="slot_var" as "slot_var" %} {% fill "my_slot" data="slot_var" default="slot_var" %}
{{ slot_var.default }} {{ slot_var }}
{% endfill %} {% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
with self.assertRaisesMessage( with self.assertRaisesMessage(
TemplateSyntaxError, TemplateSyntaxError,
"'fill' received the same string for slot alias (as ...) and slot data (data=...)", "'fill' received the same string for slot default (default=...) and slot data (data=...)",
): ):
Template(template).render(Context()) Template(template).render(Context())
@ -2535,7 +2583,7 @@ class ScopedSlotTest(BaseTestCase):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %} {% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
</div> </div>
""" """
@ -2585,7 +2633,7 @@ class ScopedSlotTest(BaseTestCase):
template: types.django_html = """ template: types.django_html = """
{% load component_tags %} {% load component_tags %}
<div> <div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %} {% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
</div> </div>
""" """
@ -2603,3 +2651,49 @@ class ScopedSlotTest(BaseTestCase):
rendered = Template(template).render(Context()) rendered = Template(template).render(Context())
expected = "<div> Default text </div>" expected = "<div> Default text </div>"
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
def test_nested_fills(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc input=input %}Default text{% endslot %}
</div>
"""
def get_context_data(self, input):
return {
"abc": "def",
"input": input,
}
template_str: types.django_html = """
{% load component_tags %}
{% component "test" input=1 %}
{% fill "my_slot" data="data1" %}
data1_in_slot1: {{ data1|safe }}
{% component "test" input=2 %}
{% fill "my_slot" data="data2" %}
data1_in_slot2: {{ data1|safe }}
data2_in_slot2: {{ data2|safe }}
{% endfill %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<div>
data1_in_slot1: {'abc': 'def', 'input': 1}
<div>
data1_in_slot2: {'abc': 'def', 'input': 1}
data2_in_slot2: {'abc': 'def', 'input': 2}
</div>
</div>
""",
)