Skip to content

Commit d7fe5cc

Browse files
JohananOppongAmoatengadamghill
authored andcommitted
addd meta for template_name
template_html form_class component_key
1 parent 7284b6c commit d7fe5cc

File tree

5 files changed

+277
-17
lines changed

5 files changed

+277
-17
lines changed

docs/source/validation.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ class BookForm(forms.Form):
2020
publish_date = forms.DateField(required=True)
2121

2222
class BookView(UnicornView):
23-
form_class = BookForm
24-
2523
title = ""
2624
publish_date = ""
25+
26+
class Meta:
27+
form_class = BookForm
2728
```
2829

2930
```html
@@ -35,7 +36,11 @@ class BookView(UnicornView):
3536
</div>
3637
```
3738

38-
Because of the `form_class = BookForm` defined on the `UnicornView` above, `Unicorn` will automatically validate that the title has a value and is less than 100 characters. The `publish_date` will also be converted into a `datetime` from the string representation in the text input.
39+
Because of the `Meta.form_class = BookForm` defined on the `UnicornView` above, `Unicorn` will automatically validate that the title has a value and is less than 100 characters. The `publish_date` will also be converted into a `datetime` from the string representation in the text input.
40+
41+
```{note}
42+
Setting `form_class` directly as a class attribute also works and is supported for backwards compatibility.
43+
```
3944

4045
### Validate the entire component
4146

@@ -60,10 +65,11 @@ class BookForm(forms.Form):
6065
title = forms.CharField(max_length=6, required=True)
6166

6267
class BookView(UnicornView):
63-
form_class = BookForm
64-
6568
text = "hello"
6669

70+
class Meta:
71+
form_class = BookForm
72+
6773
def set_text(self):
6874
self.text = "hello world"
6975
self.validate()
@@ -80,10 +86,11 @@ class BookForm(forms.Form):
8086
title = forms.CharField(max_length=6, required=True)
8187

8288
class BookView(UnicornView):
83-
form_class = BookForm
84-
8589
text = "hello"
8690

91+
class Meta:
92+
form_class = BookForm
93+
8794
def set_text(self):
8895
if self.is_valid():
8996
self.text = "hello world"

docs/source/views.md

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,32 @@ Never put sensitive data into a public property because that information will pu
125125

126126
### template_name
127127

128-
By default, the component name is used to determine what template should be used. For example, `hello_world.HelloWorldView` would by default use `unicorn/hello-world.html`. However, you can specify a particular template by setting `template_name` in the component.
128+
By default, the component name is used to determine what template should be used. For example, `hello_world.HelloWorldView` would by default use `unicorn/hello-world.html`. Set `template_name` inside `Meta` to override it.
129129

130130
```python
131131
# hello_world.py
132132
from django_unicorn.components import UnicornView
133133

134134
class HelloWorldView(UnicornView):
135-
template_name = "unicorn/hello-world.html"
135+
class Meta:
136+
template_name = "unicorn/hello-world.html"
137+
```
138+
139+
```{note}
140+
Setting `template_name` directly as a class attribute also works and is supported for backwards compatibility.
136141
```
137142

138143
### template_html
139144

140-
Template HTML can be defined inline on the component instead of using an external HTML file.
145+
Template HTML can be defined inline on the component instead of using an external HTML file. Set it inside `Meta`.
141146

142147
```python
143148
# hello_world.py
144149
from django_unicorn.components import UnicornView
145150

146151
class HelloWorldView(UnicornView):
147-
template_html = """<div>
152+
class Meta:
153+
template_html = """<div>
148154
<div>
149155
Count: {{ count }}
150156
</div>
@@ -157,6 +163,10 @@ class HelloWorldView(UnicornView):
157163
...
158164
```
159165

166+
```{note}
167+
Setting `template_html` directly as a class attribute also works and is supported for backwards compatibility.
168+
```
169+
160170
## Instance properties
161171

162172
### component_args
@@ -532,6 +542,80 @@ data, modifying records) should still verify `self.request.user.is_authenticated
532542
inside the relevant component methods.
533543
```
534544

545+
### template_name
546+
547+
Override the template path used to render the component.
548+
549+
```python
550+
# hello_world.py
551+
from django_unicorn.components import UnicornView
552+
553+
class HelloWorldView(UnicornView):
554+
class Meta:
555+
template_name = "unicorn/hello-world.html"
556+
```
557+
558+
### template_html
559+
560+
Define the component template as an inline HTML string instead of a separate file.
561+
562+
```python
563+
# hello_world.py
564+
from django_unicorn.components import UnicornView
565+
566+
class HelloWorldView(UnicornView):
567+
count = 0
568+
569+
class Meta:
570+
template_html = """<div>
571+
<div>Count: {{ count }}</div>
572+
<button unicorn:click="increment">+</button>
573+
</div>"""
574+
```
575+
576+
### component_key
577+
578+
Set a default key for the component class. This is applied when the template tag
579+
does not supply a `key=` argument and is useful when you always want a specific
580+
component to be keyed the same way.
581+
582+
```python
583+
# signup.py
584+
from django_unicorn.components import UnicornView
585+
586+
class SignupView(UnicornView):
587+
class Meta:
588+
component_key = "signup"
589+
```
590+
591+
```{note}
592+
A `key=` value provided in the template tag always takes precedence over
593+
`Meta.component_key`.
594+
```
595+
596+
### form_class
597+
598+
Attach a Django form for validation. Errors from the form are merged into the
599+
component's `errors` dict.
600+
601+
```python
602+
# book_form.py
603+
from django_unicorn.components import UnicornView
604+
from .forms import BookForm
605+
606+
class BookFormView(UnicornView):
607+
title = ""
608+
year = None
609+
610+
class Meta:
611+
form_class = BookForm
612+
```
613+
614+
```{note}
615+
Setting `form_class` directly as a class attribute also works and is supported for
616+
backwards compatibility.
617+
```
618+
535619
## Pickling and Caching
536620

537621
Components are pickled and cached for the duration of the AJAX request. This means that any instance variable on the component must be pickleable.

src/django_unicorn/components/unicorn_view.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,21 +237,38 @@ def __init__(self, component_args: list | None = None, **kwargs):
237237
self._init_script: str = ""
238238
self._validate_called = False
239239
self.errors: dict[Any, Any] = {}
240+
241+
# Apply Meta.component_key as a class-level default when the template
242+
# tag has not provided a key (i.e. self.component_key is still empty).
243+
if not self.component_key and hasattr(self, "Meta") and hasattr(self.Meta, "component_key"):
244+
self.component_key = self.Meta.component_key
245+
240246
self._set_default_template_name()
241247
self._set_caches()
242248

243249
@timed
244250
def _set_default_template_name(self) -> None:
245251
"""Sets a default template name based on component's name if necessary.
246252
247-
Also handles `template_html` if it is set on the component which overrides `template_name`.
253+
Also handles `template_html` (via Meta or direct attribute) which overrides
254+
`template_name`. Meta attributes take precedence over direct class attributes.
248255
"""
249256

250-
if hasattr(self, "template_html"):
257+
# Resolve template_html — Meta takes precedence over direct attribute
258+
template_html = None
259+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_html"):
260+
template_html = self.Meta.template_html
261+
elif hasattr(self, "template_html"):
262+
template_html = self.template_html
263+
264+
if template_html:
251265
try:
252-
self.template_name = create_template(self.template_html) # type: ignore
266+
self.template_name = create_template(template_html) # type: ignore
253267
except AssertionError:
254268
pass
269+
elif hasattr(self, "Meta") and hasattr(self.Meta, "template_name"):
270+
# Meta.template_name overrides a direct class attribute when set
271+
self.template_name = self.Meta.template_name
255272

256273
get_template_names_is_valid = False
257274

@@ -578,9 +595,15 @@ def get_frontend_context_variables(self) -> str:
578595

579596
@timed
580597
def _get_form(self, data):
581-
if hasattr(self, "form_class"):
598+
form_class = None
599+
if hasattr(self, "Meta") and hasattr(self.Meta, "form_class"):
600+
form_class = self.Meta.form_class
601+
elif hasattr(self, "form_class"):
602+
form_class = self.form_class
603+
604+
if form_class:
582605
try:
583-
form = cast(Callable, self.form_class)(data=data)
606+
form = cast(Callable, form_class)(data=data)
584607
form.is_valid()
585608

586609
return form
@@ -871,13 +894,26 @@ def _is_public(self, name: str) -> bool:
871894
"calling",
872895
"called",
873896
"login_not_required",
897+
"form_class",
874898
)
875899
excludes = []
876900

877901
if hasattr(self, "Meta") and hasattr(self.Meta, "login_not_required"):
878902
if not isinstance(self.Meta.login_not_required, bool):
879903
raise AssertionError("Meta.login_not_required should be a bool")
880904

905+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_name"):
906+
if not isinstance(self.Meta.template_name, str):
907+
raise AssertionError("Meta.template_name should be a str")
908+
909+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_html"):
910+
if not isinstance(self.Meta.template_html, str):
911+
raise AssertionError("Meta.template_html should be a str")
912+
913+
if hasattr(self, "Meta") and hasattr(self.Meta, "component_key"):
914+
if not isinstance(self.Meta.component_key, str):
915+
raise AssertionError("Meta.component_key should be a str")
916+
881917
if hasattr(self, "Meta") and hasattr(self.Meta, "exclude"):
882918
if not is_non_string_sequence(self.Meta.exclude):
883919
raise AssertionError("Meta.exclude should be a list, tuple, or set")

0 commit comments

Comments
 (0)