Skip to content

Commit f726fa4

Browse files
authored
Feature/preview (#3)
* add component preview * add preview doc
1 parent 8ac2e08 commit f726fa4

File tree

20 files changed

+411
-10
lines changed

20 files changed

+411
-10
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3-
## 1.0.1
3+
## 1.0.3
4+
5+
1. Add component `Preview` support
6+
7+
## 1.0.2
48

59
1. Initial release
132 KB
Loading
242 KB
Loading

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ Topics
1515
slot.md
1616
templates.md
1717
context.md
18+
preview.md
1819
testing.md

docs/source/preview.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Previews
2+
3+
We can create previews for our components, and check the result in the browser, just like Storybook.
4+
5+
## Config
6+
7+
Add below config to your Django settings
8+
9+
```python
10+
VIEW_COMPONENTS = {
11+
"preview_base": ["django_app/tests/previews"],
12+
}
13+
```
14+
15+
`preview_base` is the base path for your previews. You can add multiple paths to it.
16+
17+
Add below url path to your Django urls
18+
19+
```python
20+
path("previews/", include("django_viewcomponent.urls")),
21+
```
22+
23+
So all the previews will work on the `/previews` path, I'd like to do that when `DEBUG` is `True`.
24+
25+
## Create a preview
26+
27+
```bash
28+
django_app/tests/
29+
├── previews
30+
│   ├── modal_preview.py
31+
│   └── tab_preview.py
32+
```
33+
34+
Notes:
35+
36+
1. I'd like to put all previews under the `tests` directory, but you can put them anywhere you like.
37+
38+
```python
39+
from django.template import Context, Template
40+
from django_viewcomponent.preview import ViewComponentPreview
41+
42+
43+
class ModalComponentPreview(ViewComponentPreview):
44+
def small_modal(self, **kwargs):
45+
template = Template(
46+
"""
47+
{% load viewcomponent_tags %}
48+
49+
{% component 'modal' size="sm" as component %}
50+
{% call component.trigger %}
51+
<button class="btn btn-blue" data-action="click->modal#open:prevent">Open Small Modal</button>
52+
{% endcall %}
53+
54+
{% call component.body %}
55+
<h2 class="mb-4 text-xl">Small Modal Content</h2>
56+
<p class="mb-4">This is an example modal dialog box.</p>
57+
{% endcall %}
58+
59+
{% call component.actions %}
60+
<button class="btn btn-white" data-action="click->modal#close:prevent">Cancel</button>
61+
<button class="btn btn-blue" data-action="click->modal#close:prevent">Close</button>
62+
{% endcall %}
63+
{% endcomponent %}
64+
""",
65+
)
66+
67+
return template.render(Context({}))
68+
```
69+
70+
Notes:
71+
72+
1. We create a `ModalComponentPreview`, which inherits from `ViewComponentPreview`.
73+
2. We defined a public method `small_modal`, which will be used to render the preview, and `small_modal` is the name of the preview.
74+
3. We can get `request.GET` from the `kwargs` parameter, and use it to render the preview.
75+
4. In most cases, we can create one preview class for one component, and create multiple public method for one preview.
76+
77+
That is it!
78+
79+
## Check the preview
80+
81+
If we check [http://127.0.0.1:8000/previews/](http://127.0.0.1:8000/previews/), all previews will be listed there.
82+
83+
![](./images/preview-index.png)
84+
85+
If we check the `small_modal` preview, we will see the result in the browser.
86+
87+
![](./images/small-modal-preview.png)
88+
89+
The great thing is, we can also see the source code of the preview, which helps us to understand how to use the component.
90+
91+
## Override Templates
92+
93+
You can also override the templates to fit your needs, please check the template files under `django_viewcomponent/templates/django_viewcomponent` to learn more.

src/django_viewcomponent/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
from pathlib import Path
66

77
from django.template.engine import Engine
8-
98
from django_viewcomponent.loaders import ComponentLoader
109

1110

12-
def autodiscover():
11+
def autodiscover_components():
1312
# Autodetect a <component>.py file in a components dir
1413
current_engine = Engine.get_default()
1514
loader = ComponentLoader(current_engine)
@@ -19,6 +18,14 @@ def autodiscover():
1918
import_component_file(path)
2019

2120

21+
def autodiscover_previews():
22+
from django_viewcomponent.app_settings import app_settings
23+
preview_base_ls = [Path(p) for p in app_settings.PREVIEW_BASE]
24+
for directory in preview_base_ls:
25+
for path in glob.iglob(str(directory / "**/*.py"), recursive=True):
26+
import_component_file(path)
27+
28+
2229
def import_component_file(path):
2330
MODULE_PATH = path
2431
MODULE_NAME = Path(path).stem

src/django_viewcomponent/app_settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
class AppSettings:
55
def __init__(self):
6-
self.settings = getattr(settings, "COMPONENTS", {})
6+
self.settings = getattr(settings, "VIEW_COMPONENTS", {})
7+
8+
@property
9+
def PREVIEW_BASE(self):
10+
return self.settings.setdefault("preview_base", [])
711

812

913
app_settings = AppSettings()

src/django_viewcomponent/apps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ class ComponentsConfig(AppConfig):
55
name = "django_viewcomponent"
66

77
def ready(self):
8-
# autodiscover components
9-
self.module.autodiscover()
8+
self.module.autodiscover_components()
9+
self.module.autodiscover_previews()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import re
2+
import inspect
3+
from django.urls import reverse
4+
from urllib.parse import urljoin
5+
6+
pattern = re.compile(r'(?<!^)(?=[A-Z])')
7+
8+
9+
def public_instance_methods(cls):
10+
return [name for name in cls.__dict__.keys() if not name.startswith('__') and callable(getattr(cls, name))]
11+
12+
13+
def camel_to_snake(name):
14+
# https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
15+
name = pattern.sub('_', name).lower()
16+
return name
17+
18+
19+
class ViewComponentPreview:
20+
21+
previews = {}
22+
preview_name = None
23+
preview_view_component_path = None
24+
25+
def __init__(self, *args, **kwargs):
26+
pass
27+
28+
def __init_subclass__(cls, **kwargs):
29+
name = cls.__name__
30+
name = name.replace("Preview", "")
31+
new_name = camel_to_snake(name)
32+
ViewComponentPreview.previews[new_name] = cls
33+
cls.preview_name = new_name
34+
cls.preview_view_component_path = inspect.getfile(cls)
35+
cls.url = urljoin(reverse('django_viewcomponent:preview-index'), cls.preview_name + '/')
36+
37+
@classmethod
38+
def examples(cls):
39+
public_methods = public_instance_methods(cls)
40+
return public_methods
41+
42+
def preview_source(self, method_name):
43+
method = getattr(self, method_name)
44+
raw_source_code = inspect.getsource(method)
45+
46+
# remove 4 spaces from the beginning of each line
47+
lines = raw_source_code.split('\n')
48+
modified_lines = [line[4:] for line in lines]
49+
modified_string = '\n'.join(modified_lines)
50+
51+
return modified_string
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/themes/prism.min.css">
8+
</head>
9+
<body>
10+
{% block content %}
11+
{% endblock %}
12+
13+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
14+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/components/prism-python.min.js"></script>
15+
</body>
16+
</html>

0 commit comments

Comments
 (0)