Skip to content

Commit 187f3c7

Browse files
authored
Feature/2 (#13)
1 parent 10d7719 commit 187f3c7

19 files changed

+647
-214
lines changed

.pre-commit-config.yaml

+4-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
repos:
2-
- repo: https://github.com/pycqa/isort
3-
rev: 5.12.0
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.6.4
44
hooks:
5-
- id: isort
5+
- id: ruff
6+
args: [ --fix ]
67
- repo: https://github.com/psf/black
78
rev: 23.10.1
89
hooks:
910
- id: black
10-
- repo: https://github.com/PyCQA/flake8
11-
rev: 6.0.0
12-
hooks:
13-
- id: flake8
14-
additional_dependencies:
15-
- flake8-bugbear
16-
- flake8-comprehensions
17-
- flake8-no-pep420
18-
- flake8-print
19-
- flake8-tidy-imports
20-
- flake8-typing-imports
2111
- repo: https://github.com/pre-commit/mirrors-mypy
2212
rev: v1.6.1
2313
hooks:

docs/source/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
# -- Project information -----------------------------------------------------
1919
project = "django-viewcomponent"
20-
copyright = f"{datetime.datetime.now().year}, Michael Yin"
20+
copyright = f"{datetime.datetime.now().year}, Michael Yin" # noqa
2121
author = "Michael Yin"
2222

2323

65 KB
Loading

docs/source/slot.md

+109-3
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ Notes:
118118
<a href="/">Post 2</a>
119119
```
120120

121-
## Linking slots with other component
121+
## Connect other component in the slot
122122

123-
This is a very powerful feature, please read it carefully.
123+
This is the **killer feature**, so please read it carefully.
124124

125125
Let's update the `BlogComponent` again
126126

@@ -160,7 +160,7 @@ class BlogComponent(component.Component):
160160
Notes:
161161

162162
1. We added a `HeaderComponent`, which accept a `classes` argument
163-
2. `header = RendersOneField(required=True, component='header')` means when `{{ self.header.value }}` is rendered, it would use the `HeaderComponent` to render the content.
163+
2. `header = RendersOneField(required=True, component='header')` means when `{{ self.header.value }}` is rendered, it would use the `HeaderComponent` component to render the content.
164164

165165
```django
166166
{% component "blog" as blog_component %}
@@ -236,3 +236,109 @@ class PostComponent(component.Component):
236236
<h1>{{ self.post.title }}</h1>
237237
"""
238238
```
239+
240+
## Separation of concerns
241+
242+
The slot field and the `component` argument can help us build components with separation of concerns.
243+
244+
With `component` argument, we can **connect** components together, in clean way.
245+
246+
![](./images/blog-components.png)
247+
248+
## Component argument in slot field
249+
250+
`component` in `RendersOneField` or `RendersManyField` supports many variable types.
251+
252+
### Component registered name
253+
254+
```python
255+
header = RendersOneField(required=True, component="header")
256+
```
257+
258+
### Component class
259+
260+
```python
261+
class BlogComponent(component.Component):
262+
class HeaderComponent(component.Component):
263+
def __init__(self, classes, **kwargs):
264+
self.classes = classes
265+
266+
template = """
267+
<h1 class="{{ self.classes }}">
268+
{{ self.content }}
269+
</h1>
270+
"""
271+
272+
class PostComponent(component.Component):
273+
def __init__(self, post, **kwargs):
274+
self.post = post
275+
276+
template = """
277+
{% load viewcomponent_tags %}
278+
279+
<h1>{{ self.post.title }}</h1>
280+
<div>{{ self.post.description }}</div>
281+
"""
282+
283+
header = RendersOneField(required=True, component=HeaderComponent)
284+
posts = RendersManyField(required=True, component=PostComponent)
285+
286+
template = """
287+
{% load viewcomponent_tags %}
288+
{{ self.header.value }}
289+
{% for post in self.posts.value %}
290+
{{ post }}
291+
{% endfor %}
292+
"""
293+
```
294+
295+
### Function which return string
296+
297+
If one component is very simple, we can use a function or lambda to return string.
298+
299+
```python
300+
class BlogComponent(component.Component):
301+
header = RendersOneField(required=True, component="header")
302+
posts = RendersManyField(
303+
required=True,
304+
component=lambda post, **kwargs: mark_safe(
305+
f"""
306+
<h1>{post.title}</h1>
307+
<div>{post.description}</div>
308+
""",
309+
),
310+
)
311+
312+
template = """
313+
{% load viewcomponent_tags %}
314+
{{ self.header.value }}
315+
{% for post in self.posts.value %}
316+
{{ post }}
317+
{% endfor %}
318+
"""
319+
```
320+
321+
Notes:
322+
323+
1. Here we use lambda function to return string from the `post` variable, so we do not need to create a Component.
324+
325+
### Function which return component instance
326+
327+
We can use function to return instance of a component.
328+
329+
```python
330+
class BlogComponent(component.Component):
331+
header = RendersOneField(required=True, component="header")
332+
posts = RendersManyField(
333+
required=True,
334+
component=lambda post: PostComponent(post=post),
335+
)
336+
337+
template = """
338+
{% load viewcomponent_tags %}
339+
{{ self.header.value }}
340+
{% for post in self.posts.value %}
341+
{{ post }}
342+
{% endfor %}
343+
"""
344+
```

pyproject.toml

+50
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,53 @@ django = ">=3.0"
2020
[build-system]
2121
requires = ["setuptools", "poetry_core>=1.0"]
2222
build-backend = "poetry.core.masonry.api"
23+
24+
[tool.ruff]
25+
lint.ignore = [
26+
# https://docs.astral.sh/ruff/rules/
27+
"ANN",
28+
"D",
29+
"ARG",
30+
"RUF",
31+
"TRY",
32+
"PTH",
33+
"PGH",
34+
"N",
35+
"S",
36+
"EM",
37+
"ERA",
38+
"RET",
39+
"PERF",
40+
"G",
41+
"PERF",
42+
"TCH",
43+
"TD",
44+
"FIX",
45+
"FBT",
46+
47+
"E501",
48+
"PLR2004",
49+
"PT004",
50+
"PLR5501",
51+
"A002",
52+
"BLE001",
53+
"B904",
54+
"SLF001",
55+
"DJ001",
56+
"DJ008",
57+
"INP001",
58+
"I001",
59+
"FA100",
60+
"FA102",
61+
"SIM118",
62+
"UP031",
63+
"PT011",
64+
]
65+
lint.select = ["ALL"]
66+
exclude = ["migrations"]
67+
68+
[tool.ruff.lint.isort]
69+
combine-as-imports = true
70+
71+
[tool.black]
72+
target_version = ['py310']

setup.cfg

-22
This file was deleted.

src/django_viewcomponent/component.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class Component:
2525
# and you can get it using self.content
2626
content = ""
2727

28-
# the name of the component
29-
component_name = None
30-
3128
# the variable name of the component in the context
3229
component_target_var = None
3330

@@ -63,7 +60,7 @@ def get_template(self) -> Template:
6360

6461
raise ImproperlyConfigured(
6562
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
66-
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
63+
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods.",
6764
)
6865

6966
def prepare_context(

src/django_viewcomponent/component_registry.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ class NotRegistered(Exception):
66
pass
77

88

9-
class ComponentRegistry(object):
9+
class ComponentRegistry:
1010
def __init__(self):
1111
self._registry = {} # component name -> component_class mapping
1212

1313
def register(self, name=None, component=None):
1414
existing_component = self._registry.get(name)
1515
if existing_component and existing_component.class_hash != component.class_hash:
1616
raise AlreadyRegistered(
17-
'The component "%s" has already been registered' % name
17+
'The component "%s" has already been registered' % name,
1818
)
1919
self._registry[name] = component
2020

src/django_viewcomponent/fields.py

+41-19
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
from typing import Optional
2-
31
from django_viewcomponent.component_registry import registry as component_registry
42

53

64
class FieldValue:
75
def __init__(
86
self,
7+
content: str,
98
dict_data: dict,
10-
component: Optional[str] = None,
9+
component: None,
1110
parent_component=None,
12-
**kwargs
1311
):
12+
self._content = content or ""
1413
self._dict_data = dict_data
15-
self._content = self._dict_data.pop("content", "")
1614
self._component = component
1715
self._parent_component = parent_component
1816

@@ -24,11 +22,43 @@ def __str__(self):
2422
return self.render()
2523

2624
def render(self):
27-
component_cls = component_registry.get(self._component)
25+
from django_viewcomponent.component import Component
26+
27+
if isinstance(self._component, str):
28+
return self._render_for_component_cls(
29+
component_registry.get(self._component),
30+
)
31+
elif not isinstance(self._component, type) and callable(self._component):
32+
# self._component is function
33+
callable_component = self._component
34+
result = callable_component(**self._dict_data)
35+
36+
if isinstance(result, str):
37+
return result
38+
elif isinstance(result, Component):
39+
# render component instance
40+
return self._render_for_component_instance(result)
41+
else:
42+
raise ValueError(
43+
f"Callable slot component must return str or Component instance. Got {result}",
44+
)
45+
elif isinstance(self._component, type) and issubclass(
46+
self._component,
47+
Component,
48+
):
49+
# self._component is Component class
50+
return self._render_for_component_cls(self._component)
51+
else:
52+
raise ValueError(f"Invalid component variable {self._component}")
53+
54+
def _render_for_component_cls(self, component_cls):
2855
component = component_cls(
2956
**self._dict_data,
3057
)
31-
component.component_name = self._component
58+
59+
return self._render_for_component_instance(component)
60+
61+
def _render_for_component_instance(self, component):
3262
component.component_context = self._parent_component.component_context
3363

3464
with component.component_context.push():
@@ -77,13 +107,9 @@ def handle_call(self, content, **kwargs):
77107

78108
class RendersOneField(BaseSlotField):
79109
def handle_call(self, content, **kwargs):
80-
value_dict = {
81-
"content": content,
82-
"parent_component": self.parent_component,
83-
**kwargs,
84-
}
85110
value_instance = FieldValue(
86-
dict_data=value_dict,
111+
content=content,
112+
dict_data={**kwargs},
87113
component=self._component,
88114
parent_component=self.parent_component,
89115
)
@@ -94,13 +120,9 @@ def handle_call(self, content, **kwargs):
94120

95121
class RendersManyField(BaseSlotField):
96122
def handle_call(self, content, **kwargs):
97-
value_dict = {
98-
"content": content,
99-
"parent_component": self.parent_component,
100-
**kwargs,
101-
}
102123
value_instance = FieldValue(
103-
dict_data=value_dict,
124+
content=content,
125+
dict_data={**kwargs},
104126
component=self._component,
105127
parent_component=self.parent_component,
106128
)

src/django_viewcomponent/preview.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def __init_subclass__(cls, **kwargs):
4242
cls.preview_name = new_name
4343
cls.preview_view_component_path = os.path.abspath(inspect.getfile(cls))
4444
cls.url = urljoin(
45-
reverse("django_viewcomponent:preview-index"), cls.preview_name + "/"
45+
reverse("django_viewcomponent:preview-index"),
46+
cls.preview_name + "/",
4647
)
4748

4849
@classmethod

0 commit comments

Comments
 (0)