From d09fcf3b5cb74aef83308ea712cb22d9851625a4 Mon Sep 17 00:00:00 2001 From: Michaelyin Date: Mon, 6 Jan 2025 14:58:42 +0800 Subject: [PATCH] chore --- docs/source/slot.md | 104 +++++++++++++++- src/django_viewcomponent/fields.py | 22 +++- .../templatetags/viewcomponent_tags.py | 11 ++ tests/test_render_field.py | 115 +++++++++++++++++- 4 files changed, 242 insertions(+), 10 deletions(-) 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 = """

- {{ site_name }} + {{ self.content }}

""" @@ -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)