Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

recursive slot call #20

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion docs/source/slot.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 %}

<td>{{ self.content }}</td>
"""


class RowComponent(component.Component):

cells = RendersManyField(component=CellComponent)

template = """
{% load viewcomponent_tags %}

<tr>
{% for cell in self.cells.value %}
{{ cell }}
{% endfor %}
</tr>
"""


class TableComponent(component.Component):

rows = RendersManyField(component=RowComponent)

template = """
{% load viewcomponent_tags %}

<table>
<tbody>
{% for row in self.rows.value %}
{{ row }}
{% endfor %}
</tbody>
</table>
"""
```

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
<h1>{{ post.title }}</h1>
{% endcall %}
{% call row_component.cells %}
<div>{{ post.description }}</div>
{% 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
<table>
<tbody>
<tr>
<td>
<h1>post title</h1>
</td>
<td>
<div>post desc</div>
</td>
</tr>
</tbody>
</table>
```

## Polymorphic slots

Expand Down
22 changes: 16 additions & 6 deletions src/django_viewcomponent/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def __init__(
self,
nodelist,
field_context,
target_var,
polymorphic_type,
polymorphic_types,
dict_data: dict,
Expand All @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand Down
11 changes: 11 additions & 0 deletions src/django_viewcomponent/templatetags/viewcomponent_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -43,6 +50,7 @@ def do_call(parser, token):
return CallNode(
parser=parser,
nodelist=nodelist,
target_var=target_var,
args=args,
kwargs=kwargs,
)
Expand All @@ -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

Expand All @@ -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(
Expand Down
115 changes: 112 additions & 3 deletions tests/test_render_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def get_context_data(self):

template = """
<h1 class="{{ self.classes }}">
<a href="/"> {{ site_name }} </a>
{{ self.content }}
</h1>
"""

Expand Down Expand Up @@ -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' %}
<a href="/"> {{ site_name }} </a>
{% endcall %}
{% for post in qs %}
{% call component.posts post=post %}{% endcall %}
{% endfor %}
Expand Down Expand Up @@ -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' %}
<a href="/"> {{ site_name }} </a>
{% endcall %}
{% for post in qs %}
{% call component.wrappers %}
<h1>{{ post.title }}</h1>
Expand Down Expand Up @@ -571,3 +575,108 @@ def test_field_component_parameter(self):
</li>
"""
assert_dom_equal(expected, rendered)


class CellComponent(component.Component):
template = """
{% load viewcomponent_tags %}

<td>{{ self.content }}</td>
"""


class RowComponent(component.Component):
cells = RendersManyField(component=CellComponent)

template = """
{% load viewcomponent_tags %}

<tr>
{% for cell in self.cells.value %}
{{ cell }}
{% endfor %}
</tr>
"""


class TableComponent(component.Component):
rows = RendersManyField(component=RowComponent)

template = """
{% load viewcomponent_tags %}

<table>
<tbody>
{% for row in self.rows.value %}
{{ row }}
{% endfor %}
</tbody>
</table>
"""


@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 %}
<h1>{{ post.title }}</h1>
{% endcall %}
{% call row_component.cells %}
<div>{{ post.description }}</div>
{% endcall %}
{% endcall %}
{% endfor %}
{% endcomponent %}

""",
)
rendered = template.render(Context({"qs": qs}))
expected = """
<table>
<tbody>
<tr>
<td>
<h1>test 0</h1>
</td>
<td>
<div>test 0</div>
</td>
</tr>
<tr>
<td>
<h1>test 1</h1>
</td>
<td>
<div>test 1</div>
</td>
</tr>
<tr>
<td>
<h1>test 2</h1>
</td>
<td>
<div>test 2</div>
</td>
</tr>
</tbody>
</table>
"""
assert_dom_equal(expected, rendered)
Loading