diff --git a/src/django_components/component.py b/src/django_components/component.py index 864b05ba..4d6782af 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -716,10 +716,6 @@ class Component( # Get the component's HTML html_content = template.render(context) - # After we've rendered the contents, we now know what slots were there, - # and thus we can validate that. - component_slot_ctx.post_render_validation() - # Allow to optionally override/modify the rendered content new_output = self.on_render_after(context, template, html_content) html_content = new_output if new_output is not None else html_content diff --git a/src/django_components/slots.py b/src/django_components/slots.py index e21f6d40..7464ca9a 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -148,41 +148,6 @@ class ComponentSlotContext: default_slot: Optional[str] fills: Dict[SlotName, Slot] - def post_render_validation(self) -> None: - if self.is_dynamic_component: - return - - default_fill = self.fills.get(DEFAULT_SLOT_KEY, None) - - # Check: Only component templates that include a 'default' slot - # can be invoked with implicit filling. - if default_fill and not self.default_slot: - raise TemplateSyntaxError( - f"Component '{self.component_name}' passed default fill content " - f"(i.e. without explicit 'name' kwarg), " - f"even though none of its slots is marked as 'default'." - ) - - # NOTE: - # In the current implementation, the slots are resolved only at the render time. - # So when we are rendering Django's Nodes, and we come across a SlotNode, only - # at that point we check if we have the fill for it. - # - # That means that we can use variables, and we can place slots in loops. - # - # However, because the slot names are dynamic, we cannot know all the slot names - # that will be rendered ahead of the time. - # - # Moreover, user may define a slot whose default content has more slots inside it. - # - # Previously, there was an error raised if there were unfilled slots or extra fills. - # - # But now this is only a message. Because: - # 1. We don't know about ALL slots, just about the rendered ones, so we CANNOT check - # for unfilled slots (rendered slots WILL raise an error if the fill is missing). - # 2. User may provide extra fills, but these may belong to slots we haven't - # encountered in this render run. So we CANNOT say which ones are extra. - class SlotNode(BaseNode): """Node corresponding to `{% slot %}`""" @@ -214,6 +179,27 @@ class SlotNode(BaseNode): def __repr__(self) -> str: return f"" + # NOTE: + # In the current implementation, the slots are resolved only at the render time. + # So when we are rendering Django's Nodes, and we come across a SlotNode, only + # at that point we check if we have the fill for it. + # + # That means that we can use variables, and we can place slots in loops. + # + # However, because the slot names are dynamic, we cannot know all the slot names + # that will be rendered ahead of the time. + # + # Moreover, user may define a `{% slot %}` whose default content has more nested + # `{% slot %}` tags inside of it. + # + # Previously, there was an error raised if there were unfilled slots or extra fills, + # or if there was an extra fill for a default slot. + # + # But we don't raise those anymore, because: + # 1. We don't know about ALL slots, just about the rendered ones, so we CANNOT check + # for unfilled slots (rendered slots WILL raise an error if the fill is missing). + # 2. User may provide extra fills, but these may belong to slots we haven't + # encountered in this render run. So we CANNOT say which ones are extra. def render(self, context: Context) -> SafeString: trace_msg("RENDR", "SLOT", self.trace_id, self.node_id) diff --git a/tests/test_templatetags_slot_fill.py b/tests/test_templatetags_slot_fill.py index 6f01d63a..1a49c3ea 100644 --- a/tests/test_templatetags_slot_fill.py +++ b/tests/test_templatetags_slot_fill.py @@ -748,23 +748,75 @@ class ComponentSlotDefaultTests(BaseTestCase): self.assertTrue(True) @parametrize_context_behavior(["django", "isolated"]) - def test_component_without_default_slot_refuses_implicit_fill(self): + def test_implicit_fill_when_no_slot_marked_default(self): registry.register("test_comp", SlottedComponent) template_str: types.django_html = """ {% load component_tags %} {% component 'test_comp' %} -

This shouldn't work because the included component doesn't mark - any of its slots as 'default'

+

Component with no 'default' slot still accepts the fill, it just won't render it

+ {% endcomponent %} + """ + template = Template(template_str) + rendered = template.render(Context()) + self.assertHTMLEqual( + rendered, + """ + +
Default header
+
Default main
+
Default footer
+
+ """, + ) + + @parametrize_context_behavior(["django", "isolated"]) + def test_implicit_fill_when_slot_marked_default_not_rendered(self): + @register("test_comp") + class ConditionalSlotted(Component): + def get_context_data(self, var: bool) -> Any: + return {"var": var} + + template: types.django_html = """ + {% load component_tags %} + + {% if var %} +
{% slot "header" default %}Default header{% endslot %}
+ {% endif %} +
{% slot "main" %}Default main{% endslot %}
+
{% slot "footer" %}Default footer{% endslot %}
+
+ """ + + template_str: types.django_html = """ + {% load component_tags %} + {% component 'test_comp' var=var %} + 123 {% endcomponent %} """ template = Template(template_str) - with self.assertRaisesMessage( - TemplateSyntaxError, - "Component 'test_comp' passed default fill content (i.e. without explicit 'name' kwarg), " - "even though none of its slots is marked as 'default'", - ): - template.render(Context()) + rendered_truthy = template.render(Context({"var": True})) + self.assertHTMLEqual( + rendered_truthy, + """ + +
123
+
Default main
+
Default footer
+
+ """, + ) + + rendered_falsy = template.render(Context({"var": False})) + self.assertHTMLEqual( + rendered_falsy, + """ + +
Default main
+ +
+ """, + ) class PassthroughSlotsTest(BaseTestCase):