diff --git a/docs/source/slot.md b/docs/source/slot.md
index 61dcc69..f68fcad 100644
--- a/docs/source/slot.md
+++ b/docs/source/slot.md
@@ -122,6 +122,8 @@ Notes:
This is the **killer feature**, so please read it carefully.
+### Component argument in RendersOneField
+
Let's update the `BlogComponent` again
```python
@@ -198,7 +200,7 @@ Notes:
1. We do not need to store the `classes` to the `BlogComponent` and then pass it to the `HeaderComponent`, just set `component='header'` in the `RendersOneField` field, the `HeaderComponent` would receive the `classes` argument automatically
2. If you check the template code in the `BlogComponent`, `{{ self.header.value }}` ia very simple to help you understand what it is.
-## Component with RendersManyField
+### Component argument in RendersManyField
If you have
@@ -245,14 +247,48 @@ With `component` argument, we can **connect** components together, in clean way.
![](./images/blog-components.png)
-## Component argument in slot field
-
-`component` in `RendersOneField` or `RendersManyField` supports many variable types.
+## Component argument in slot fields supports different variable types
### Component registered name
```python
-header = RendersOneField(required=True, component="header")
+@component.register("header")
+class HeaderComponent(component.Component):
+ def __init__(self, classes, **kwargs):
+ self.classes = classes
+
+ template = """
+
+ {{ self.content }}
+
+ """
+
+
+@component.register("post")
+class PostComponent(component.Component):
+ def __init__(self, post, **kwargs):
+ self.post = post
+
+ template = """
+ {% load viewcomponent_tags %}
+
+ {{ self.post.title }}
+ {{ self.post.description }}
+ """
+
+
+@component.register("blog")
+class BlogComponent(component.Component):
+ header = RendersOneField(required=True, component="header")
+ posts = RendersManyField(required=True, component="post")
+
+ template = """
+ {% load viewcomponent_tags %}
+ {{ self.header.value }}
+ {% for post in self.posts.value %}
+ {{ post }}
+ {% endfor %}
+ """
```
### Component class
@@ -301,7 +337,7 @@ class BlogComponent(component.Component):
header = RendersOneField(required=True, component="header")
posts = RendersManyField(
required=True,
- component=lambda post, **kwargs: mark_safe(
+ component=lambda self, post, **kwargs: mark_safe(
f"""
{post.title}
{post.description}
@@ -321,17 +357,18 @@ class BlogComponent(component.Component):
Notes:
1. Here we use lambda function to return string from the `post` variable, so we do not need to create a Component.
+2. We can still use `self.xxx` to access value of the blog component.
### Function which return component instance
-We can use function to return instance of a component.
+We can use function to return instance of a component, this is useful when we need to pass some special default values to the other component.
```python
class BlogComponent(component.Component):
header = RendersOneField(required=True, component="header")
posts = RendersManyField(
required=True,
- component=lambda post: PostComponent(post=post),
+ component=lambda post, **kwargs: PostComponent(post=post),
)
template = """
diff --git a/src/django_viewcomponent/fields.py b/src/django_viewcomponent/fields.py
index 026cf78..8f329d0 100644
--- a/src/django_viewcomponent/fields.py
+++ b/src/django_viewcomponent/fields.py
@@ -4,22 +4,18 @@
class FieldValue:
def __init__(
self,
- content: str,
+ nodelist,
dict_data: dict,
component: None,
parent_component=None,
):
- self._content = content or ""
+ self._nodelist = nodelist
self._dict_data = dict_data
self._component = component
self._parent_component = parent_component
def __str__(self):
- if self._component is None:
- return self._content
- else:
- # If the slot field is defined with component, then we will use the component to render
- return self.render()
+ return self.render()
def render(self):
from django_viewcomponent.component import Component
@@ -31,7 +27,10 @@ def render(self):
elif not isinstance(self._component, type) and callable(self._component):
# self._component is function
callable_component = self._component
- result = callable_component(**self._dict_data)
+ result = callable_component(
+ self=self._parent_component,
+ **self._dict_data,
+ )
if isinstance(result, str):
return result
@@ -48,6 +47,8 @@ def render(self):
):
# self._component is Component class
return self._render_for_component_cls(self._component)
+ elif self._component is None:
+ return self._nodelist.render(self._parent_component.component_context)
else:
raise ValueError(f"Invalid component variable {self._component}")
@@ -67,7 +68,7 @@ def _render_for_component_instance(self, component):
# create slot fields
component.create_slot_fields()
- component.content = self._content
+ component.content = self._nodelist.render(updated_context)
component.check_slot_fields()
@@ -101,14 +102,14 @@ def filled(self):
def required(self):
return self._required
- def handle_call(self, content, **kwargs):
+ def handle_call(self, nodelist, **kwargs):
raise NotImplementedError("You must implement the `handle_call` method.")
class RendersOneField(BaseSlotField):
- def handle_call(self, content, **kwargs):
+ def handle_call(self, nodelist, **kwargs):
value_instance = FieldValue(
- content=content,
+ nodelist=nodelist,
dict_data={**kwargs},
component=self._component,
parent_component=self.parent_component,
@@ -118,17 +119,35 @@ def handle_call(self, content, **kwargs):
self._value = value_instance
+class FieldValueListWrapper:
+ """
+ This helps render FieldValue eagerly when component template has
+ {% for panel in self.panels.value %}, this can avoid issues if `panel` of the for loop statement
+ # override context variables in some cases.
+ """
+
+ def __init__(self):
+ self.data = []
+
+ def append(self, value):
+ self.data.append(value)
+
+ def __iter__(self):
+ for field_value in self.data:
+ yield field_value.render()
+
+
class RendersManyField(BaseSlotField):
- def handle_call(self, content, **kwargs):
+ def handle_call(self, nodelist, **kwargs):
value_instance = FieldValue(
- content=content,
+ nodelist=nodelist,
dict_data={**kwargs},
component=self._component,
parent_component=self.parent_component,
)
if self._value is None:
- self._value = []
+ self._value = FieldValueListWrapper()
self._value.append(value_instance)
self._filled = True
diff --git a/src/django_viewcomponent/templatetags/viewcomponent_tags.py b/src/django_viewcomponent/templatetags/viewcomponent_tags.py
index 518cbc6..a69088d 100644
--- a/src/django_viewcomponent/templatetags/viewcomponent_tags.py
+++ b/src/django_viewcomponent/templatetags/viewcomponent_tags.py
@@ -65,8 +65,6 @@ def __repr__(self):
raise NotImplementedError
def render(self, context):
- content = self.nodelist.render(context)
-
resolved_kwargs = {
key: safe_resolve(kwarg, context) for key, kwarg in self.kwargs.items()
}
@@ -76,7 +74,7 @@ def render(self, context):
"The 'content' kwarg is reserved and cannot be passed in component call tag",
)
- resolved_kwargs["content"] = content
+ resolved_kwargs["nodelist"] = self.nodelist
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 e9c0117..f769171 100644
--- a/tests/test_render_field.py
+++ b/tests/test_render_field.py
@@ -8,6 +8,103 @@
from tests.utils import assert_dom_equal
+@pytest.mark.django_db
+class TestRenderFieldComponentContextLogic:
+ """
+ HeaderComponent.get_context_data add extra context data
+
+ We can still access the value via {{ site_name }}
+ """
+
+ class HeaderComponent(component.Component):
+ def __init__(self, classes, **kwargs):
+ self.classes = classes
+
+ def get_context_data(self):
+ context = super().get_context_data()
+ context["site_name"] = "My Site"
+ return context
+
+ template = """
+
+ {{ self.content }}
+
+ """
+
+ class PostComponent(component.Component):
+ def __init__(self, post, **kwargs):
+ self.post = post
+
+ template = """
+ {% load viewcomponent_tags %}
+
+ {{ self.post.title }}
+ {{ self.post.description }}
+ """
+
+ class BlogComponent(component.Component):
+ header = RendersOneField(required=True, component="header")
+ posts = RendersManyField(required=True, component="post")
+
+ template = """
+ {% load viewcomponent_tags %}
+ {{ self.header.value }}
+ {% for post in self.posts.value %}
+ {{ post }}
+ {% endfor %}
+ """
+
+ @pytest.fixture(autouse=True)
+ def register_component(self):
+ component.registry.register("blog", self.BlogComponent)
+ component.registry.register("header", self.HeaderComponent)
+ component.registry.register("post", self.PostComponent)
+
+ def test_field_context_logic(self):
+ for i in range(5):
+ 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 'blog' as component %}
+ {% call component.header classes='text-lg' %}
+ {{ site_name }}
+ {% endcall %}
+ {% for post in qs %}
+ {% call component.posts post=post %}{% endcall %}
+ {% endfor %}
+ {% endcomponent %}
+ """,
+ )
+ rendered = template.render(Context({"qs": qs}))
+ expected = """
+
+
+ test 0
+ test 0
+
+ test 1
+ test 1
+
+ test 2
+ test 2
+
+ test 3
+ test 3
+
+ test 4
+ test 4
+ """
+ assert_dom_equal(expected, rendered)
+
+
@pytest.mark.django_db
class TestRenderFieldComponentParameterString:
"""
@@ -98,7 +195,7 @@ def test_field_component_parameter(self):
assert_dom_equal(expected, rendered)
-class BlogComponent(component.Component):
+class BlogComponent1(component.Component):
class HeaderComponent(component.Component):
def __init__(self, classes, **kwargs):
self.classes = classes
@@ -140,7 +237,7 @@ class TestRenderFieldComponentParameterClass:
@pytest.fixture(autouse=True)
def register_component(self):
- component.registry.register("blog", BlogComponent)
+ component.registry.register("blog", BlogComponent1)
def test_field_component_parameter(self):
for i in range(5):
@@ -204,12 +301,15 @@ def __init__(self, classes, **kwargs):
"""
class BlogComponent(component.Component):
+ def __init__(self, **kwargs):
+ self.foo = "Hello"
+
header = RendersOneField(required=True, component="header")
posts = RendersManyField(
required=True,
- component=lambda post, **kwargs: mark_safe(
+ component=lambda self, post, **kwargs: mark_safe(
f"""
- {post.title}
+ {self.foo} {post.title}
{post.description}
""",
),
@@ -255,25 +355,25 @@ def test_field_component_parameter(self):
My Site
- test 0
+ Hello test 0
test 0
- test 1
+ Hello test 1
test 1
- test 2
+ Hello test 2
test 2
- test 3
+ Hello test 3
test 3
- test 4
+ Hello test 4
test 4
"""
assert_dom_equal(expected, rendered)
-class PostComponent(component.Component):
+class PostComponent2(component.Component):
def __init__(self, post, **kwargs):
self.post = post
@@ -305,7 +405,7 @@ class BlogComponent(component.Component):
header = RendersOneField(required=True, component="header")
posts = RendersManyField(
required=True,
- component=lambda post: PostComponent(post=post),
+ component=lambda post, **kwargs: PostComponent2(post=post),
)
template = """