Skip to content

Commit ef2f382

Browse files
authored
use components in python (#11)
1 parent ff0be30 commit ef2f382

File tree

11 files changed

+363
-8
lines changed

11 files changed

+363
-8
lines changed

.github/assets/component-in-py.png

150 KB
Loading
256 KB
Loading

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ It is inspired by Rails [ViewComponent](https://viewcomponent.org/), which built
99

1010
For more insights into the problem it addresses and its design philosophy, check out [this video by GitHub Staff member Joel Hawksley](https://youtu.be/QoetqsBCsbE?si=28PCFCD4N4CyfKY7&t=624)
1111

12+
## Use Component in Django Template
13+
14+
You can create components and use them in Django templates.
15+
16+
![Use Component in Django Template](.github/assets/component-in-template.png)
17+
18+
## Use Component in Python
19+
20+
Or you can create components and use them in pure Python code.
21+
22+
![Use Component in Python](.github/assets/component-in-py.png)
23+
1224
## Why use django-viewcomponent
1325

1426
### Single responsibility

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Topics
1616
templates.md
1717
context.md
1818
namespace.md
19+
use_components_in_python.md
1920
preview.md
2021
testing.md
2122
articles.md
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Use Components in Python
2+
3+
With django-viewcomponent, you can also create components and use them in pure Python code.
4+
5+
```python
6+
class Div(component.Component):
7+
template_name = "layout/div.html"
8+
css_class = None
9+
10+
def __init__(self, *fields, dom_id=None, css_class=None):
11+
self.fields = list(fields)
12+
if self.css_class and css_class:
13+
self.css_class += f" {css_class}"
14+
elif css_class:
15+
self.css_class = css_class
16+
self.dom_id = dom_id
17+
18+
def get_context_data(self):
19+
context = super().get_context_data()
20+
self.fields_html = " ".join(
21+
[
22+
child_component.render_from_parent_context(context)
23+
for child_component in self.fields
24+
]
25+
)
26+
return context
27+
```
28+
29+
This is a `Div` component, it will accept a list of child components and set them in `self.fields`
30+
31+
In `get_context_data`, it will pass `context` to each child component and render them to HTML using `render_from_parent_context` method.
32+
33+
Then in `layout/div.html`, the child components will be rendered using `{{ self.fields_html|safe }}`
34+
35+
You can find more examples in the `tests` folder.
36+
37+
With this approach, you can use components in Python code like this
38+
39+
```python
40+
Layout(
41+
Fieldset(
42+
"Basic Info",
43+
Field("first_name"),
44+
Field("last_name"),
45+
Field("password1"),
46+
Field("password2"),
47+
css_class="fieldset",
48+
),
49+
)
50+
```

src/django_viewcomponent/component.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Component:
2121
template_name: ClassVar[Optional[str]] = None
2222
template: ClassVar[Optional[str]] = None
2323

24-
# if pass HTML to the component without fill tags, it will be stored here
24+
# if pass HTML to the component without calling slot fields, it will be stored here
2525
# and you can get it using self.content
2626
content = ""
2727

@@ -32,18 +32,18 @@ class Component:
3232
component_target_var = None
3333

3434
# the context of the component, generated by get_context_data
35-
component_context: Dict["str", Any] = {}
35+
component_context: Context = Context({})
3636

3737
# the context of the outer
38-
outer_context: Dict["str", Any] = {}
38+
outer_context: Context = Context({})
3939

4040
def __init__(self, *args, **kwargs):
4141
pass
4242

4343
def __init_subclass__(cls, **kwargs):
4444
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
4545

46-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
46+
def get_context_data(self, **kwargs) -> Context:
4747
# inspired by rails viewcomponent before_render method
4848
# developers can add extra context data by overriding this method
4949
self.outer_context["self"] = self
@@ -71,18 +71,57 @@ def get_template(self) -> Template:
7171
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
7272
)
7373

74-
def render(
74+
def prepare_context(
7575
self,
7676
context_data: Union[Dict[str, Any], Context, None] = None,
77-
) -> str:
77+
) -> Context:
78+
"""
79+
Prepare the context data for rendering the component.
80+
81+
https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render
82+
"""
7883
context_data = context_data or {}
7984
if isinstance(context_data, dict):
8085
context = Context(context_data)
8186
else:
8287
context = context_data
8388

89+
return context
90+
91+
def render(
92+
self,
93+
context_data: Union[Dict[str, Any], Context, None] = None,
94+
) -> str:
8495
template = self.get_template()
85-
return template.render(context)
96+
return template.render(self.prepare_context(context_data))
97+
98+
def render_from_parent_context(self, parent_context=None):
99+
"""
100+
If developers build components in Python code, then slot fields can be ignored, this method
101+
help simplify rendering the child components
102+
103+
Div(
104+
Fieldset(
105+
"Basic Info",
106+
Field('first_name'),
107+
Field('last_name'),
108+
Field('email'),
109+
css_class='fieldset',
110+
),
111+
Submit('Submit'),
112+
dom_id="main",
113+
)
114+
"""
115+
parent_context = parent_context or {}
116+
# create isolated context for component
117+
if isinstance(parent_context, Context):
118+
copied_context = Context(parent_context.flatten())
119+
else:
120+
copied_context = Context(dict(parent_context))
121+
122+
self.outer_context = self.prepare_context(copied_context)
123+
self.component_context = self.get_context_data()
124+
return self.render(self.component_context)
86125

87126
def check_slot_fields(self):
88127
# check required slot fields

tests/templates/layout/button.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<button
2+
class="{{ self.field_classes }}"
3+
type="{{ self.button_type }}"
4+
{% if self.id %}id="{{ self.id }}"{% endif %}
5+
>
6+
{{ self.text_html }}
7+
</button>

tests/templates/layout/div.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div {% if self.dom_id %}id="{{ self.dom_id }}"{% endif %} {% if self.css_class %}class="{{ self.css_class }}"{% endif %} >
2+
{{ self.fields_html|safe }}
3+
</div>

tests/test_layout.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from tests.testapp.layout import HTML, Button, Div
2+
3+
from .utils import assert_select
4+
5+
6+
class TestLayoutComponents:
7+
def test_html(self):
8+
html = HTML("{% if saved %}Data saved{% endif %}").render_from_parent_context(
9+
{"saved": True}
10+
)
11+
assert "Data saved" in html
12+
13+
# step_field and step0 not defined
14+
html = HTML(
15+
'<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />'
16+
).render_from_parent_context()
17+
assert_select(html, "input")
18+
19+
def test_div(self):
20+
html = Div(
21+
Div(
22+
HTML("Hello {{ value_1 }}"),
23+
HTML("Hello {{ value_2 }}"),
24+
css_class="wrapper",
25+
),
26+
dom_id="main",
27+
).render_from_parent_context({"value_1": "world"})
28+
29+
assert_select(html, "div#main")
30+
assert_select(html, "div.wrapper")
31+
assert "Hello world" in html
32+
33+
def test_button(self):
34+
html = Div(
35+
Div(
36+
Button("{{ value_1 }}", css_class="btn btn-primary"),
37+
),
38+
dom_id="main",
39+
).render_from_parent_context({"value_1": "world"})
40+
41+
assert_select(html, "button.btn")
42+
assert_select(html, "button[type=button]")
43+
assert "world" in html

tests/testapp/layout.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.template import Template
2+
3+
from django_viewcomponent import component
4+
5+
6+
class Div(component.Component):
7+
template_name = "layout/div.html"
8+
css_class = None
9+
10+
def __init__(self, *fields, dom_id=None, css_class=None):
11+
self.fields = list(fields)
12+
if self.css_class and css_class:
13+
self.css_class += f" {css_class}"
14+
elif css_class:
15+
self.css_class = css_class
16+
self.dom_id = dom_id
17+
18+
def get_context_data(self):
19+
context = super().get_context_data()
20+
self.fields_html = " ".join(
21+
[
22+
child_component.render_from_parent_context(context)
23+
for child_component in self.fields
24+
]
25+
)
26+
return context
27+
28+
29+
class HTML(component.Component):
30+
def __init__(self, html, **kwargs):
31+
self.html = html
32+
33+
def get_template(self) -> Template:
34+
return Template(self.html)
35+
36+
37+
class Button(component.Component):
38+
template_name = "layout/button.html"
39+
field_classes = "btn"
40+
button_type = "button"
41+
42+
def __init__(self, text, dom_id=None, css_class=None, template=None):
43+
self.text = text
44+
45+
if dom_id:
46+
self.id = dom_id
47+
48+
self.attrs = {}
49+
50+
if css_class:
51+
self.field_classes += f" {css_class}"
52+
53+
self.template_name = template or self.template_name
54+
55+
def get_context_data(self):
56+
context = super().get_context_data()
57+
self.text_html = Template(str(self.text)).render(context)
58+
return context

0 commit comments

Comments
 (0)