Skip to content

Commit 02e109f

Browse files
authored
Feature/polymorphic (#17)
1 parent 2fb688e commit 02e109f

File tree

5 files changed

+157
-32
lines changed

5 files changed

+157
-32
lines changed

Diff for: docs/source/slot.md

+33
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,36 @@ class BlogComponent(component.Component):
394394
{% endfor %}
395395
"""
396396
```
397+
398+
399+
## Polymorphic slots
400+
401+
Polymorphic slots can render one of several possible slots.
402+
403+
For example, consider this list item component that can be rendered with either an icon or an avatar.
404+
405+
```python
406+
class ListItemComponent(component.Component):
407+
item = RendersOneField(
408+
types={
409+
"avatar": AvatarComponent,
410+
"icon": lambda key, **kwargs: IconComponent(key=key),
411+
},
412+
)
413+
```
414+
415+
The values in `types` can be the same as `component` argument in `RendersOneField`, it can be a string, a class, a function
416+
417+
Filling the slot is done calling `{field}_{type}`, please note the `type` key is set as `suffix` to the slot field.
418+
419+
```django
420+
{% load viewcomponent_tags %}
421+
422+
{% component 'list_item' as component %}
423+
{% call component.item_avatar alt='username' src='http://some-site.com/my_avatar.jpg' %}{% endcall %}
424+
{% endcomponent %}
425+
426+
{% component 'list_item' as component %}
427+
{% call component.item_icon key='arrow-down' %}{% endcall %}
428+
{% endcomponent %}
429+
```

Diff for: pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ lint.ignore = [
6161
"SIM118",
6262
"UP031",
6363
"PT011",
64+
"PLR0913",
6465
]
6566
lint.select = ["ALL"]
6667
exclude = ["migrations"]

Diff for: src/django_viewcomponent/fields.py

+44-20
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ def __init__(
66
self,
77
nodelist,
88
field_context,
9+
polymorphic_type,
10+
polymorphic_types,
911
dict_data: dict,
1012
component: None,
1113
parent_component=None,
1214
):
1315
self._nodelist = nodelist
1416
self._field_context = field_context
17+
self._polymorphic_type = polymorphic_type
18+
self._polymorphic_types = polymorphic_types
1519
self._dict_data = dict_data
1620
self._component = component
1721
self._parent_component = parent_component
@@ -20,17 +24,27 @@ def __str__(self):
2024
return self.render()
2125

2226
def render(self):
27+
if self._polymorphic_types:
28+
component_expression = self._polymorphic_types[self._polymorphic_type]
29+
return self._render(component_expression)
30+
else:
31+
component_expression = self._component
32+
return self._render(component_expression)
33+
34+
def _render(self, target):
2335
from django_viewcomponent.component import Component
2436

25-
if isinstance(self._component, str):
37+
if isinstance(target, str):
2638
return self._render_for_component_cls(
27-
component_registry.get(self._component),
39+
component_registry.get(target),
2840
)
29-
elif not isinstance(self._component, type) and callable(self._component):
30-
# self._component is function
31-
callable_component = self._component
41+
elif not isinstance(target, type) and callable(target):
42+
# target is function
43+
callable_component = target
44+
content = self._nodelist.render(self._field_context)
3245
result = callable_component(
3346
self=self._parent_component,
47+
content=content,
3448
**self._dict_data,
3549
)
3650

@@ -43,16 +57,16 @@ def render(self):
4357
raise ValueError(
4458
f"Callable slot component must return str or Component instance. Got {result}",
4559
)
46-
elif isinstance(self._component, type) and issubclass(
47-
self._component,
60+
elif isinstance(target, type) and issubclass(
61+
target,
4862
Component,
4963
):
50-
# self._component is Component class
51-
return self._render_for_component_cls(self._component)
52-
elif self._component is None:
64+
# target is Component class
65+
return self._render_for_component_cls(target)
66+
elif target is None:
5367
return self._nodelist.render(self._field_context)
5468
else:
55-
raise ValueError(f"Invalid component variable {self._component}")
69+
raise ValueError(f"Invalid component variable {target}")
5670

5771
def _render_for_component_cls(self, component_cls):
5872
component = component_cls(
@@ -62,29 +76,31 @@ def _render_for_component_cls(self, component_cls):
6276
return self._render_for_component_instance(component)
6377

6478
def _render_for_component_instance(self, component):
65-
component.component_context = self._parent_component.component_context
79+
component.component_context = self._field_context
6680

6781
with component.component_context.push():
68-
updated_context = component.get_context_data()
69-
7082
# create slot fields
7183
component.create_slot_fields()
7284

73-
component.content = self._nodelist.render(updated_context)
85+
# render content first
86+
component.content = self._nodelist.render(component.component_context)
7487

7588
component.check_slot_fields()
7689

90+
updated_context = component.get_context_data()
91+
7792
return component.render(updated_context)
7893

7994

8095
class BaseSlotField:
8196
parent_component = None
8297

83-
def __init__(self, value=None, required=False, component=None, **kwargs):
84-
self._value = value
98+
def __init__(self, required=False, component=None, types=None, **kwargs):
99+
self._value = None
85100
self._filled = False
86101
self._required = required
87102
self._component = component
103+
self._types = types
88104

89105
@classmethod
90106
def initialize_fields(cls):
@@ -104,15 +120,21 @@ def filled(self):
104120
def required(self):
105121
return self._required
106122

107-
def handle_call(self, nodelist, context, **kwargs):
123+
@property
124+
def types(self):
125+
return self._types
126+
127+
def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
108128
raise NotImplementedError("You must implement the `handle_call` method.")
109129

110130

111131
class RendersOneField(BaseSlotField):
112-
def handle_call(self, nodelist, context, **kwargs):
132+
def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
113133
value_instance = FieldValue(
114134
nodelist=nodelist,
115135
field_context=context,
136+
polymorphic_type=polymorphic_type,
137+
polymorphic_types=self.types,
116138
dict_data={**kwargs},
117139
component=self._component,
118140
parent_component=self.parent_component,
@@ -134,10 +156,12 @@ def __iter__(self):
134156

135157

136158
class RendersManyField(BaseSlotField):
137-
def handle_call(self, nodelist, context, **kwargs):
159+
def handle_call(self, nodelist, context, polymorphic_type, **kwargs):
138160
value_instance = FieldValue(
139161
nodelist=nodelist,
140162
field_context=context,
163+
polymorphic_type=polymorphic_type,
164+
polymorphic_types=self.types,
141165
dict_data={**kwargs},
142166
component=self._component,
143167
parent_component=self.parent_component,

Diff for: src/django_viewcomponent/templatetags/viewcomponent_tags.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,29 @@ def render(self, context):
8484
if not component_instance:
8585
raise ValueError(f"Component {component_token} not found in context")
8686

87-
field = getattr(component_instance, field_token, None)
88-
if not field:
87+
available_slot_fields_map = {}
88+
# iterate all attributes of the component instance and add the BaseSlotField to the list
89+
for field_name in dir(component_instance):
90+
field = getattr(component_instance, field_name)
91+
if isinstance(field, BaseSlotField):
92+
types = field.types
93+
if types:
94+
for polymorphic_type in types:
95+
available_slot_fields_map[
96+
f"{field_name}_{polymorphic_type}"
97+
] = [field, polymorphic_type]
98+
else:
99+
available_slot_fields_map[field_name] = [field, None]
100+
101+
if field_token not in available_slot_fields_map:
89102
raise ValueError(
90103
f"Field {field_token} not found in component {component_token}",
91104
)
92105

93-
if isinstance(field, BaseSlotField):
94-
# call field
95-
return field.handle_call(**resolved_kwargs) or ""
106+
field, polymorphic_type = available_slot_fields_map[field_token]
107+
resolved_kwargs["polymorphic_type"] = polymorphic_type
108+
109+
return field.handle_call(**resolved_kwargs) or ""
96110

97111

98112
class ComponentNode(Node):

Diff for: tests/test_render_field.py

+60-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def get_context_data(self):
2121

2222
template = """
2323
<h1 class="{{ self.classes }}">
24-
{{ self.content }}
24+
<a href="/"> {{ site_name }} </a>
2525
</h1>
2626
"""
2727

@@ -76,9 +76,7 @@ def test_field_context_logic(self):
7676
"""
7777
{% load viewcomponent_tags %}
7878
{% component 'blog' as component %}
79-
{% call component.header classes='text-lg' %}
80-
<a href="/"> {{ site_name }} </a>
81-
{% endcall %}
79+
{% call component.header classes='text-lg' %}{% endcall %}
8280
{% for post in qs %}
8381
{% call component.posts post=post %}{% endcall %}
8482
{% endfor %}
@@ -123,9 +121,7 @@ def test_field_context_logic_2(self):
123121
"""
124122
{% load viewcomponent_tags %}
125123
{% component 'blog' as component %}
126-
{% call component.header classes='text-lg' %}
127-
<a href="/"> {{ site_name }} </a>
128-
{% endcall %}
124+
{% call component.header classes='text-lg' %}{% endcall %}
129125
{% for post in qs %}
130126
{% call component.wrappers %}
131127
<h1>{{ post.title }}</h1>
@@ -518,3 +514,60 @@ def test_field_component_parameter(self):
518514
<div>test 4</div>
519515
"""
520516
assert_dom_equal(expected, rendered)
517+
518+
519+
@pytest.mark.django_db
520+
class TestRenderFieldTypes:
521+
"""
522+
Polymorphic slots
523+
"""
524+
525+
class AvatarComponent(component.Component):
526+
def __init__(self, src, alt, **kwargs):
527+
self.src = src
528+
self.alt = alt
529+
530+
template = """
531+
<img src="{{ self.src }}" alt="{{ self.alt }}">
532+
"""
533+
534+
class ListItemComponent(component.Component):
535+
item = RendersOneField(
536+
required=True,
537+
types={
538+
"avatar": "avatar",
539+
"span": lambda content, **kwargs: mark_safe(f"<span>{content}</span>"),
540+
},
541+
)
542+
543+
template = """
544+
<li>{{ self.item.value }}</li>
545+
"""
546+
547+
@pytest.fixture(autouse=True)
548+
def register_component(self):
549+
component.registry.register("list_item", self.ListItemComponent)
550+
component.registry.register("avatar", self.AvatarComponent)
551+
552+
def test_field_component_parameter(self):
553+
template = Template(
554+
"""
555+
{% load viewcomponent_tags %}
556+
{% component 'list_item' as component %}
557+
{% call component.item_avatar alt='username' src='http://some-site.com/my_avatar.jpg' %}{% endcall %}
558+
{% endcomponent %}
559+
{% component 'list_item' as component %}
560+
{% call component.item_span %}username{% endcall %}
561+
{% endcomponent %}
562+
""",
563+
)
564+
rendered = template.render(Context({}))
565+
expected = """
566+
<li>
567+
<img src="http://some-site.com/my_avatar.jpg" alt="username">
568+
</li>
569+
<li>
570+
<span>username</span>
571+
</li>
572+
"""
573+
assert_dom_equal(expected, rendered)

0 commit comments

Comments
 (0)