-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented @htmx_form_validate and the rest of form_validation.rst
- Loading branch information
1 parent
0a5354d
commit dce5429
Showing
7 changed files
with
261 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
from functools import wraps | ||
|
||
from django.forms import Form | ||
from django.forms.widgets import CheckboxInput | ||
from django.http import HttpResponse | ||
from django.http.request import QueryDict | ||
|
||
|
||
def htmx_form_validate(*, form_class: type): | ||
""" | ||
Instead of a normal view, just do htmx validation using the given form class, | ||
for a single field and return the single div that needs to be replaced. | ||
Normally the form class will be the same class used in the view body. | ||
""" | ||
|
||
def decorator(view_func): | ||
@wraps(view_func) | ||
def wrapper(request, *args, **kwargs): | ||
if ( | ||
request.method == "POST" | ||
and "Hx-Request" in request.headers | ||
and (htmx_validation_field := request.POST.get("_validate_field", None)) | ||
): | ||
form = _build_validation_form(form_class, request.POST, htmx_validation_field) | ||
form.is_valid() # trigger validation | ||
return HttpResponse(render_single_field_row(form, htmx_validation_field)) | ||
return view_func(request, *args, **kwargs) | ||
|
||
return wrapper | ||
|
||
return decorator | ||
|
||
|
||
def render_single_field_row(form: Form, field_name: str): | ||
# Assumes form has renderer with `single_field_row_template` defined | ||
bound_field = form[field_name] | ||
return form.render( | ||
context={ | ||
"field": bound_field, | ||
"errors": form.error_class(bound_field.errors, renderer=form.renderer), | ||
"do_htmx_validation": form.do_htmx_validation, | ||
}, | ||
template_name=form.renderer.single_field_row_template, | ||
) | ||
|
||
|
||
def _build_validation_form(form_class: type[Form], data: QueryDict, field_name: str): | ||
# htmx quirk: when using hx-params, it submits "undefined" for checkboxes that are unchecked, | ||
# instead of omitting the field like we expect. We want to keep using params, to avoid submitting | ||
# fields that are not relevant to individual field validation, so we fix here. | ||
field = form_class.base_fields[field_name] | ||
if isinstance(field.widget, CheckboxInput): | ||
if data.get(field_name) == "undefined": | ||
data = data.copy() | ||
del data[field_name] # checkbox value is False | ||
return form_class(data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{% load widget_tweaks %} | ||
|
||
<div | ||
{% with classes=field.css_classes %} class="field is-horizontal {{ classes }}" | ||
{% endwith %} | ||
id="form-row-{{ field.name }}" | ||
{% if do_htmx_validation and field|widget_type != "fileinput" %} | ||
hx-post="." | ||
hx-vals='{"_validate_field": "{{ field.name }}" }' | ||
hx-trigger="change from:#form-row-{{ field.name }}" | ||
hx-params="{{ field.name }},_validate_field" | ||
hx-target="this" | ||
hx-swap="outerHTML" | ||
{% endif %} | ||
> | ||
{% if field.label %} | ||
<div class="field-label is-normal"> | ||
{{ field.label_tag }} | ||
</div> | ||
{% endif %} | ||
<div class="field-body"> | ||
{% with error_class=errors|yesno:"is-danger,," %} | ||
<div class="field"> | ||
<div class="control"> | ||
{% if field|widget_type == "select" %} | ||
<div class="select {{ error_class }}"> | ||
{{ field }} | ||
</div> | ||
{% else %} | ||
{{ field|add_class:error_class }} | ||
{% endif %} | ||
</div> | ||
{% if field.help_text %} | ||
<p class="help">{{ field.help_text|safe }}</p> | ||
{% endif %} | ||
{% if errors %} | ||
<div class="help is-danger"> | ||
{{ errors }} | ||
</div> | ||
{% endif %} | ||
</div> | ||
{% endwith %} | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.