diff --git a/docs/source/slot.md b/docs/source/slot.md
index 070ff68..07ed622 100644
--- a/docs/source/slot.md
+++ b/docs/source/slot.md
@@ -133,9 +133,13 @@ Or you can use django for loop do this:
{% endcomponent %}
```
+```{note}
+Developer can use this approach to fill the slot field in a flexible way.
+```
+
## Connect other component in the slot
-This is the **killer feature**, so please read it carefully.
+This is the **killer feature** of this package, so please read it carefully.
### Component argument in RendersOneField
@@ -395,6 +399,104 @@ class BlogComponent(component.Component):
"""
```
+## Recursive Slot Field Call
+
+Combining render fields and `component` argument is very powerful, let's step further to see how to do recursive slot field call.
+
+Let's assume you are building a generic table components:
+
+```
+Table
+ Row
+ Cell
+```
+
+Below is code example:
+
+```python
+class CellComponent(component.Component):
+
+ template = """
+ {% load viewcomponent_tags %}
+
+
{{ self.content }} |
+ """
+
+
+class RowComponent(component.Component):
+
+ cells = RendersManyField(component=CellComponent)
+
+ template = """
+ {% load viewcomponent_tags %}
+
+
+ {% for cell in self.cells.value %}
+ {{ cell }}
+ {% endfor %}
+
+ """
+
+
+class TableComponent(component.Component):
+
+ rows = RendersManyField(component=RowComponent)
+
+ template = """
+ {% load viewcomponent_tags %}
+
+
+
+ {% for row in self.rows.value %}
+ {{ row }}
+ {% endfor %}
+
+
+ """
+```
+
+1. `TableComponent.rows -> RowComponent`
+2. `RowComponent.cells -> CellComponent`
+
+To render the table, we can do it like this:
+
+```django
+{% load viewcomponent_tags %}
+
+{% component 'table' as table_component %}
+ {% for post in qs %}
+ {% call table_component.rows as row_component %} -> Here we get the component of the slot field as `row_component`
+ {% call row_component.cells %} -> We just fill the slot field by calling row_component.cells
+ {{ post.title }}
+ {% endcall %}
+ {% call row_component.cells %}
+ {{ post.description }}
+ {% endcall %}
+ {% endcall %}
+ {% endfor %}
+{% endcomponent %}
+```
+
+Notes:
+
+1. To render `table cell`, we do not need to explicitly use `{% component 'table_cell' %}`, but using `{% call row_component.cells %}` to do this in elegant way.
+
+The final HTML would seem like:
+
+```html
+
+
+
+
+ post title
+ |
+
+ post desc
+ |
+
+
+
+```
## Polymorphic slots
diff --git a/src/django_viewcomponent/fields.py b/src/django_viewcomponent/fields.py
index a684abb..50304be 100644
--- a/src/django_viewcomponent/fields.py
+++ b/src/django_viewcomponent/fields.py
@@ -6,6 +6,7 @@ def __init__(
self,
nodelist,
field_context,
+ target_var,
polymorphic_type,
polymorphic_types,
dict_data: dict,
@@ -14,6 +15,7 @@ def __init__(
):
self._nodelist = nodelist
self._field_context = field_context
+ self._target_var = target_var
self._polymorphic_type = polymorphic_type
self._polymorphic_types = polymorphic_types
self._dict_data = dict_data
@@ -76,19 +78,25 @@ def _render_for_component_cls(self, component_cls):
return self._render_for_component_instance(component)
def _render_for_component_instance(self, component):
+ """
+ The logic should be the same as in the ComponentNode.render method
+ """
+ component.component_target_var = self._target_var
component.component_context = self._field_context
+ # https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context.push
with component.component_context.push():
+ # developer can add extra context data in this method
+ updated_context = component.get_context_data()
+
# create slot fields
component.create_slot_fields()
# render content first
- component.content = self._nodelist.render(component.component_context)
+ component.content = self._nodelist.render(updated_context)
component.check_slot_fields()
- updated_context = component.get_context_data()
-
return component.render(updated_context)
@@ -124,15 +132,16 @@ def required(self):
def types(self):
return self._types
- def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
+ def handle_call(self, nodelist, context, target_var, polymorphic_type, **kwargs):
raise NotImplementedError("You must implement the `handle_call` method.")
class RendersOneField(BaseSlotField):
- def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
+ def handle_call(self, nodelist, context, target_var, polymorphic_type, **kwargs):
value_instance = FieldValue(
nodelist=nodelist,
field_context=context,
+ target_var=target_var,
polymorphic_type=polymorphic_type,
polymorphic_types=self.types,
dict_data={**kwargs},
@@ -156,10 +165,11 @@ def __iter__(self):
class RendersManyField(BaseSlotField):
- def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
+ def handle_call(self, nodelist, context, target_var, polymorphic_type, **kwargs):
value_instance = FieldValue(
nodelist=nodelist,
field_context=context,
+ target_var=target_var,
polymorphic_type=polymorphic_type,
polymorphic_types=self.types,
dict_data={**kwargs},
diff --git a/src/django_viewcomponent/templatetags/viewcomponent_tags.py b/src/django_viewcomponent/templatetags/viewcomponent_tags.py
index 352652c..23ed945 100644
--- a/src/django_viewcomponent/templatetags/viewcomponent_tags.py
+++ b/src/django_viewcomponent/templatetags/viewcomponent_tags.py
@@ -16,6 +16,13 @@
@register.tag("call")
def do_call(parser, token):
bits = token.split_contents()
+
+ # check as keyword
+ target_var = None
+ if len(bits) >= 4 and bits[-2] == "as":
+ target_var = bits[-1]
+ bits = bits[:-2]
+
tag_name = "call"
tag_args, tag_kwargs = parse_bits(
parser=parser,
@@ -43,6 +50,7 @@ def do_call(parser, token):
return CallNode(
parser=parser,
nodelist=nodelist,
+ target_var=target_var,
args=args,
kwargs=kwargs,
)
@@ -53,11 +61,13 @@ def __init__(
self,
parser,
nodelist: NodeList,
+ target_var,
args,
kwargs,
):
self.parser = parser
self.nodelist: NodeList = nodelist
+ self.target_var = target_var
self.args = args
self.kwargs = kwargs
@@ -76,6 +86,7 @@ def render(self, context):
resolved_kwargs["nodelist"] = self.nodelist
resolved_kwargs["context"] = context
+ resolved_kwargs["target_var"] = self.target_var
component_token, field_token = self.args[0].token.split(".")
component_instance = FilterExpression(component_token, self.parser).resolve(
diff --git a/tests/test_render_field.py b/tests/test_render_field.py
index 39c5f79..a9a581f 100644
--- a/tests/test_render_field.py
+++ b/tests/test_render_field.py
@@ -21,7 +21,7 @@ def get_context_data(self):
template = """
"""
@@ -76,7 +76,9 @@ def test_field_context_logic(self):
"""
{% load viewcomponent_tags %}
{% component 'blog' as component %}
- {% call component.header classes='text-lg' %}{% endcall %}
+ {% call component.header classes='text-lg' %}
+ {{ site_name }}
+ {% endcall %}
{% for post in qs %}
{% call component.posts post=post %}{% endcall %}
{% endfor %}
@@ -121,7 +123,9 @@ def test_field_context_logic_2(self):
"""
{% load viewcomponent_tags %}
{% component 'blog' as component %}
- {% call component.header classes='text-lg' %}{% endcall %}
+ {% call component.header classes='text-lg' %}
+ {{ site_name }}
+ {% endcall %}
{% for post in qs %}
{% call component.wrappers %}
{{ post.title }}
@@ -571,3 +575,108 @@ def test_field_component_parameter(self):
"""
assert_dom_equal(expected, rendered)
+
+
+class CellComponent(component.Component):
+ template = """
+ {% load viewcomponent_tags %}
+
+ {{ self.content }} |
+ """
+
+
+class RowComponent(component.Component):
+ cells = RendersManyField(component=CellComponent)
+
+ template = """
+ {% load viewcomponent_tags %}
+
+
+ {% for cell in self.cells.value %}
+ {{ cell }}
+ {% endfor %}
+
+ """
+
+
+class TableComponent(component.Component):
+ rows = RendersManyField(component=RowComponent)
+
+ template = """
+ {% load viewcomponent_tags %}
+
+
+
+ {% for row in self.rows.value %}
+ {{ row }}
+ {% endfor %}
+
+
+ """
+
+
+@pytest.mark.django_db
+class TestRecursiveSlotCall:
+ @pytest.fixture(autouse=True)
+ def register_component(self):
+ component.registry.register("table", TableComponent)
+
+ def test_recursive_slot_call(self):
+ for i in range(3):
+ title = f"test {i}"
+ description = f"test {i}"
+ Post.objects.create(title=title, description=description)
+
+ qs = Post.objects.all()
+
+ template = Template(
+ """
+ {% load viewcomponent_tags %}
+
+ {% component 'table' as table_component %}
+ {% for post in qs %}
+ {% call table_component.rows as row_component %}
+ {% call row_component.cells %}
+ {{ post.title }}
+ {% endcall %}
+ {% call row_component.cells %}
+ {{ post.description }}
+ {% endcall %}
+ {% endcall %}
+ {% endfor %}
+ {% endcomponent %}
+
+ """,
+ )
+ rendered = template.render(Context({"qs": qs}))
+ expected = """
+
+
+
+
+ test 0
+ |
+
+ test 0
+ |
+
+
+
+ test 1
+ |
+
+ test 1
+ |
+
+
+
+ test 2
+ |
+
+ test 2
+ |
+
+
+
+ """
+ assert_dom_equal(expected, rendered)