diff --git a/README.md b/README.md index 1e49cfa3..58c7f303 100644 --- a/README.md +++ b/README.md @@ -1311,7 +1311,7 @@ tags: {% endcomponent %} ``` -You can also use `{% for %}`, `{% with %}`, or other tags (even `{% include %}`) +You can also use `{% for %}`, `{% with %}`, or other non-component tags (even `{% include %}`) to construct the `{% fill %}` tags, **as long as these other tags do not leave any text behind!** ```django diff --git a/src/django_components/component.py b/src/django_components/component.py index 5741b756..864b05ba 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -57,6 +57,7 @@ from django_components.slots import ( SlotName, SlotRef, SlotResult, + _is_extracting_fill, _nodelist_to_slot_render_func, resolve_fills, ) @@ -894,6 +895,11 @@ class ComponentNode(BaseNode): def render(self, context: Context) -> str: trace_msg("RENDR", "COMP", self.name, self.node_id) + # Do not render nested `{% component %}` tags in other `{% component %}` tags + # at the stage when we are determining if the latter has named fills or not. + if _is_extracting_fill(context): + return "" + component_cls: Type[Component] = self.registry.get(self.name) # Resolve FilterExpressions and Variables that were passed as args to the diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 09a1c5f5..e21f6d40 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -399,7 +399,7 @@ class FillNode(BaseNode): self.trace_id = trace_id def render(self, context: Context) -> str: - if self._is_extracting_fill(context): + if _is_extracting_fill(context): self._extract_fill(context) return "" @@ -459,9 +459,6 @@ class FillNode(BaseNode): return value - def _is_extracting_fill(self, context: Context) -> bool: - return context.get(FILL_GEN_CONTEXT_KEY, None) is not None - def _extract_fill(self, context: Context) -> None: # `FILL_GEN_CONTEXT_KEY` is only ever set when we are rendering content between the # `{% component %}...{% endcomponent %}` tags. This is done in order to collect all fill tags. @@ -775,3 +772,7 @@ def _nodelist_to_slot_render_func( return rendered return Slot(content_func=cast(SlotFunc, render_func)) + + +def _is_extracting_fill(context: Context) -> bool: + return context.get(FILL_GEN_CONTEXT_KEY, None) is not None diff --git a/src/docs/concepts/fundamentals/slots.md b/src/docs/concepts/fundamentals/slots.md index 557dd668..05b6e12b 100644 --- a/src/docs/concepts/fundamentals/slots.md +++ b/src/docs/concepts/fundamentals/slots.md @@ -77,7 +77,7 @@ tags: {% endcomponent %} ``` -You can also use `{% for %}`, `{% with %}`, or other tags (even `{% include %}`) +You can also use `{% for %}`, `{% with %}`, or other non-component tags (even `{% include %}`) to construct the `{% fill %}` tags, **as long as these other tags do not leave any text behind!** ```django diff --git a/tests/test_templatetags_provide.py b/tests/test_templatetags_provide.py index 34a25ac0..6e95765c 100644 --- a/tests/test_templatetags_provide.py +++ b/tests/test_templatetags_provide.py @@ -1,3 +1,5 @@ +from typing import Any + from django.template import Context, Template, TemplateSyntaxError from django_components import Component, register, types @@ -737,3 +739,68 @@ class InjectTest(BaseTestCase): comp = InjectComponent("") with self.assertRaises(RuntimeError): comp.inject("abc", "def") + + @parametrize_context_behavior(["django", "isolated"]) + def test_inject_in_fill(self): + @register("injectee") + class Injectee(Component): + template: types.django_html = """ + {% load component_tags %} +
injected: {{ data|safe }}
+
+ {% slot "content" default / %} +
+ """ + + def get_context_data(self): + data = self.inject("my_provide") + return {"data": data} + + @register("provider") + class Provider(Component): + def get_context_data(self, data: Any) -> Any: + return {"data": data} + + template: types.django_html = """ + {% load component_tags %} + {% provide "my_provide" key="hi" data=data %} + {% slot "content" default / %} + {% endprovide %} + """ + + @register("parent") + class Parent(Component): + def get_context_data(self, data: Any) -> Any: + return {"data": data} + + template: types.django_html = """ + {% load component_tags %} + {% component "provider" data=data %} + {% component "injectee" %} + {% slot "content" default / %} + {% endcomponent %} + {% endcomponent %} + """ + + @register("root") + class Root(Component): + template: types.django_html = """ + {% load component_tags %} + {% component "parent" data=123 %} + {% fill "content" %} + 456 + {% endfill %} + {% endcomponent %} + """ + + rendered = Root.render() + + self.assertHTMLEqual( + rendered, + """ +
+ injected: DepInject(key='hi', data=123) +
+
456
+ """, + )