diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 7cf02b1d..6d25263f 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -417,13 +417,14 @@ def _collect_slot_fills_from_component_template( continue slot_name = node.name + + # If true then the template contains multiple slot of the same name. + # No action needed, since even tho there's mutliple slots, we will + # still apply only a single fill to all of them. And each slot handles + # their own fallback content. if slot_name in slot_name2fill_content: - raise TemplateSyntaxError( - f"Slot name '{slot_name}' re-used within the same template. " - f"Slot names must be unique." - f"To fix, check template '{template.name}' " - f"of component '{registered_name}'." - ) + continue + if node.is_required: required_slot_names.add(node.name) diff --git a/tests/templates/template_with_nonunique_slots_nested.html b/tests/templates/template_with_nonunique_slots_nested.html new file mode 100644 index 00000000..ce53e83e --- /dev/null +++ b/tests/templates/template_with_nonunique_slots_nested.html @@ -0,0 +1,16 @@ +{% load component_tags %} +{% slot "header" %}START{% endslot %} +
+ {% component "calendar" date="2020-06-06" %} + {% fill "header" %} {# fills and slots with same name relate to diff. things. #} + {% slot "header" %}NESTED{% endslot %} + {% endfill %} + {% fill "body" %}Here are your to-do items for today:{% endfill %} + {% endcomponent %} +
    + {% for item in items %} +
  1. {{ item }}
  2. + {% slot "header" %}LOOP {{ item }} {% endslot %} + {% endfor %} +
+
diff --git a/tests/test_component.py b/tests/test_component.py index c83c401b..03e5c99d 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -1,6 +1,6 @@ import sys from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from django.core.exceptions import ImproperlyConfigured from django.template import Context, Template @@ -38,11 +38,33 @@ def get_context_data(self, shadowing_variable=None, new_variable=None): return context +class DuplicateSlotComponent(component.Component): + template_name = "template_with_nonunique_slots.html" + + def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: + return { + "name": name, + } + + +class DuplicateSlotNestedComponent(component.Component): + template_name = "template_with_nonunique_slots_nested.html" + + def get_context_data(self, items: List) -> Dict[str, Any]: + return { + "items": items, + } + +class CalendarComponent(component.Component): + """Nested in ComponentWithNestedComponent""" + + template_name = "slotted_component_nesting_template_pt1_calendar.html" + + ######################### # TESTS ######################### - class ComponentTest(BaseTestCase): @classmethod def setUpClass(cls): @@ -386,6 +408,137 @@ def get_context_data(self, name: Optional[str] = None) -> Dict[str, Any]: ) +class DuplicateSlotTest(BaseTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + component.registry.register(name="duplicate_slot", component=DuplicateSlotComponent) + component.registry.register(name="duplicate_slot_nested", component=DuplicateSlotNestedComponent) + component.registry.register(name="calendar", component=CalendarComponent) + + def test_duplicate_slots(self): + self.template = Template( + """ + {% load component_tags %} + {% component "duplicate_slot" %} + {% fill "header" %} + Name: {{ name }} + {% endfill %} + {% fill "footer" %} + Hello + {% endfill %} + {% endcomponent %} + """ + ) + + rendered = self.template.render(Context({"name": "Jannete"})) + self.assertHTMLEqual( + rendered, + """ +
Name: Jannete
+
Name: Jannete
+ + """, + ) + + def test_duplicate_slots_fallback(self): + self.template = Template( + """ + {% load component_tags %} + {% component "duplicate_slot" %} + {% endcomponent %} + """ + ) + rendered = self.template.render(Context({})) + + # NOTE: Slots should have different fallbacks even though they use the same name + self.assertHTMLEqual( + rendered, + """ +
Default header
+
Default main header
+ + """, + ) + + def test_duplicate_slots_nested(self): + self.template = Template( + """ + {% load component_tags %} + {% component "duplicate_slot_nested" items=items %} + {% fill "header" %} + OVERRIDDEN! + {% endfill %} + {% endcomponent %} + """ + ) + rendered = self.template.render(Context({"items": [1, 2, 3]})) + + # NOTE: Slots should have different fallbacks even though they use the same name + self.assertHTMLEqual( + rendered, + """ + OVERRIDDEN! +
+
+

+ OVERRIDDEN! +

+
+ Here are your to-do items for today: +
+
+ +
    +
  1. 1
  2. + OVERRIDDEN! +
  3. 2
  4. + OVERRIDDEN! +
  5. 3
  6. + OVERRIDDEN! +
+
+ """, + ) + + def test_duplicate_slots_nested_fallback(self): + self.template = Template( + """ + {% load component_tags %} + {% component "duplicate_slot_nested" items=items %} + {% endcomponent %} + """ + ) + rendered = self.template.render(Context({"items": [1, 2, 3]})) + + # NOTE: Slots should have different fallbacks even though they use the same name + self.assertHTMLEqual( + rendered, + """ + START +
+
+

+ NESTED +

+
+ Here are your to-do items for today: +
+
+ +
    +
  1. 1
  2. + LOOP 1 +
  3. 2
  4. + LOOP 2 +
  5. 3
  6. + LOOP 3 +
+
+ """, + ) + + class InlineComponentTest(BaseTestCase): def test_inline_html_component(self): class InlineHTMLComponent(component.Component): diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index ab8ea7d8..10f63de8 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -941,7 +941,6 @@ def setUpClass(cls): super().setUpClass() component.registry.register("test", SlottedComponent) component.registry.register("broken_component", BrokenComponent) - component.registry.register("nonunique_slot_component", NonUniqueSlotsComponent) @classmethod def tearDownClass(cls) -> None: @@ -1031,16 +1030,6 @@ def test_non_unique_fill_names_is_error(self): """ ).render(Context({})) - def test_non_unique_slot_names_is_error(self): - with self.assertRaises(TemplateSyntaxError): - Template( - """ - {% load component_tags %} - {% component "nonunique_slot_component" %} - {% endcomponent %} - """ - ).render(Context({})) - class ComponentNestingTests(BaseTestCase): @classmethod