Skip to content

Commit 8f39b91

Browse files
addd meta for template_name
template_html form_class component_key
1 parent 501dadb commit 8f39b91

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
@@ -488,6 +498,80 @@ data, modifying records) should still verify `self.request.user.is_authenticated
488498
inside the relevant component methods.
489499
```
490500

501+
### template_name
502+
503+
Override the template path used to render the component.
504+
505+
```python
506+
# hello_world.py
507+
from django_unicorn.components import UnicornView
508+
509+
class HelloWorldView(UnicornView):
510+
class Meta:
511+
template_name = "unicorn/hello-world.html"
512+
```
513+
514+
### template_html
515+
516+
Define the component template as an inline HTML string instead of a separate file.
517+
518+
```python
519+
# hello_world.py
520+
from django_unicorn.components import UnicornView
521+
522+
class HelloWorldView(UnicornView):
523+
count = 0
524+
525+
class Meta:
526+
template_html = """<div>
527+
<div>Count: {{ count }}</div>
528+
<button unicorn:click="increment">+</button>
529+
</div>"""
530+
```
531+
532+
### component_key
533+
534+
Set a default key for the component class. This is applied when the template tag
535+
does not supply a `key=` argument and is useful when you always want a specific
536+
component to be keyed the same way.
537+
538+
```python
539+
# signup.py
540+
from django_unicorn.components import UnicornView
541+
542+
class SignupView(UnicornView):
543+
class Meta:
544+
component_key = "signup"
545+
```
546+
547+
```{note}
548+
A `key=` value provided in the template tag always takes precedence over
549+
`Meta.component_key`.
550+
```
551+
552+
### form_class
553+
554+
Attach a Django form for validation. Errors from the form are merged into the
555+
component's `errors` dict.
556+
557+
```python
558+
# book_form.py
559+
from django_unicorn.components import UnicornView
560+
from .forms import BookForm
561+
562+
class BookFormView(UnicornView):
563+
title = ""
564+
year = None
565+
566+
class Meta:
567+
form_class = BookForm
568+
```
569+
570+
```{note}
571+
Setting `form_class` directly as a class attribute also works and is supported for
572+
backwards compatibility.
573+
```
574+
491575
## Pickling and Caching
492576

493577
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
@@ -228,21 +228,38 @@ def __init__(self, component_args: list | None = None, **kwargs):
228228
self._init_script: str = ""
229229
self._validate_called = False
230230
self.errors: dict[Any, Any] = {}
231+
232+
# Apply Meta.component_key as a class-level default when the template
233+
# tag has not provided a key (i.e. self.component_key is still empty).
234+
if not self.component_key and hasattr(self, "Meta") and hasattr(self.Meta, "component_key"):
235+
self.component_key = self.Meta.component_key
236+
231237
self._set_default_template_name()
232238
self._set_caches()
233239

234240
@timed
235241
def _set_default_template_name(self) -> None:
236242
"""Sets a default template name based on component's name if necessary.
237243
238-
Also handles `template_html` if it is set on the component which overrides `template_name`.
244+
Also handles `template_html` (via Meta or direct attribute) which overrides
245+
`template_name`. Meta attributes take precedence over direct class attributes.
239246
"""
240247

241-
if hasattr(self, "template_html"):
248+
# Resolve template_html — Meta takes precedence over direct attribute
249+
template_html = None
250+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_html"):
251+
template_html = self.Meta.template_html
252+
elif hasattr(self, "template_html"):
253+
template_html = self.template_html
254+
255+
if template_html:
242256
try:
243-
self.template_name = create_template(self.template_html) # type: ignore
257+
self.template_name = create_template(template_html) # type: ignore
244258
except AssertionError:
245259
pass
260+
elif hasattr(self, "Meta") and hasattr(self.Meta, "template_name"):
261+
# Meta.template_name overrides a direct class attribute when set
262+
self.template_name = self.Meta.template_name
246263

247264
get_template_names_is_valid = False
248265

@@ -560,9 +577,15 @@ def get_frontend_context_variables(self) -> str:
560577

561578
@timed
562579
def _get_form(self, data):
563-
if hasattr(self, "form_class"):
580+
form_class = None
581+
if hasattr(self, "Meta") and hasattr(self.Meta, "form_class"):
582+
form_class = self.Meta.form_class
583+
elif hasattr(self, "form_class"):
584+
form_class = self.form_class
585+
586+
if form_class:
564587
try:
565-
form = cast(Callable, self.form_class)(data=data)
588+
form = cast(Callable, form_class)(data=data)
566589
form.is_valid()
567590

568591
return form
@@ -853,13 +876,26 @@ def _is_public(self, name: str) -> bool:
853876
"calling",
854877
"called",
855878
"login_not_required",
879+
"form_class",
856880
)
857881
excludes = []
858882

859883
if hasattr(self, "Meta") and hasattr(self.Meta, "login_not_required"):
860884
if not isinstance(self.Meta.login_not_required, bool):
861885
raise AssertionError("Meta.login_not_required should be a bool")
862886

887+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_name"):
888+
if not isinstance(self.Meta.template_name, str):
889+
raise AssertionError("Meta.template_name should be a str")
890+
891+
if hasattr(self, "Meta") and hasattr(self.Meta, "template_html"):
892+
if not isinstance(self.Meta.template_html, str):
893+
raise AssertionError("Meta.template_html should be a str")
894+
895+
if hasattr(self, "Meta") and hasattr(self.Meta, "component_key"):
896+
if not isinstance(self.Meta.component_key, str):
897+
raise AssertionError("Meta.component_key should be a str")
898+
863899
if hasattr(self, "Meta") and hasattr(self.Meta, "exclude"):
864900
if not is_non_string_sequence(self.Meta.exclude):
865901
raise AssertionError("Meta.exclude should be a list, tuple, or set")

0 commit comments

Comments
 (0)