Source code for django_htmx_modal_forms.views
"""Django HTMX Modal Forms - Class-based views for HTMX-powered Bootstrap modals."""
from typing import Any, Optional
from django.forms import BaseForm
from django.http import HttpResponse
from django.http.request import HttpRequest
from django.shortcuts import render
from django.views.generic.base import View
from django.views.generic.edit import CreateView, UpdateView
from django_htmx.http import (
HttpResponseClientRefresh,
reswap,
retarget,
trigger_client_event,
)
[docs]
class HtmxModalFormMixin(View):
"""
Base mixin for modal form handling with HTMX and Bootstrap.
This mixin provides the core functionality for handling forms in Bootstrap modals
using HTMX for dynamic updates.
Attributes:
template_name (str): Template for the full modal structure
form_template_name (str): Template for just the form content
modal_size (str): Bootstrap modal size class (sm, lg, xl)
modal_title (str): Custom modal title override
"""
template_name = "htmx_modal_forms/_modal_form.html"
form_template_name = "htmx_modal_forms/_form_content.html"
modal_size: str = "lg"
modal_title: Optional[str] = None
element_id_prefix: Optional[str] = None
element_id_suffix: Optional[str] = None
[docs]
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
"""Handle GET requests and trigger modal show."""
response = super().get(request, *args, **kwargs)
return trigger_client_event(response, "modal:show", after="swap")
[docs]
def get_modal_title(self) -> str:
"""
Get the title for the modal.
Override this method to customize the modal title.
"""
if self.modal_title:
return self.modal_title
if hasattr(self, "object") and self.object:
return f"Edit {self.object}"
return f"Add {self.model._meta.verbose_name.title()}"
[docs]
def get_modal_size(self) -> str:
"""
Get the Bootstrap modal size class.
Override this method to customize the modal size.
"""
return self.modal_size
[docs]
def get_element_id(self) -> str:
"""
Get the element ID for the form target.
This method can be overridden to customize the ID generation.
By default, it uses the pattern: "{prefix}{model_name}-{pk}{suffix}"
"""
prefix = self.element_id_prefix or ""
suffix = self.element_id_suffix or ""
base_id = f"{self.model._meta.model_name}-{self.object.pk}"
return f"{prefix}{base_id}{suffix}"
[docs]
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"""Add modal-specific context."""
context = super().get_context_data(**kwargs)
context.update(
{
"modal_title": self.get_modal_title(),
"modal_size": self.get_modal_size(),
"form_url": self.request.get_full_path(),
}
)
return context
[docs]
def form_invalid(self, form: BaseForm) -> HttpResponse:
"""Handle invalid form submission."""
context = self.get_context_data(form=form)
response = render(self.request, self.form_template_name, context)
response = reswap(response, "outerHTML")
return retarget(response, "[data-form-content]")
[docs]
class HtmxModalCreateView(HtmxModalFormMixin, CreateView):
"""
View for creating objects via modal forms.
On successful form submission, the page will be refreshed to show
the newly created object.
"""
[docs]
def form_valid(self, form: BaseForm) -> HttpResponse:
"""Save form and refresh page on success."""
self.object = form.save()
return HttpResponseClientRefresh()
[docs]
class HtmxModalUpdateView(HtmxModalFormMixin, UpdateView):
"""
View for updating objects via modal forms.
On successful form submission, the target element will be updated
with the new object details.
Attributes:
detail_template_name (str): Template for rendering updated object details
"""
detail_template_name: Optional[str] = None
[docs]
def get_detail_template_name(self) -> str:
"""
Get the template name for rendering object details.
Override this method to customize the template selection.
"""
if self.detail_template_name is None:
raise ValueError("detail_template_name is required for HtmxModalUpdateView")
return self.detail_template_name
[docs]
def form_valid(self, form: BaseForm) -> HttpResponse:
"""Save form and update the page with new object details."""
self.object = form.save()
# Render updated object details
context = self.get_context_data()
detail_html = render(
self.request, self.get_detail_template_name(), context
).content.decode()
# Create response with out-of-band swap
response = HttpResponse(
f"""
<div id="{self.get_element_id()}"
hx-swap-oob="true">
{detail_html}
</div>
"""
)
# Trigger modal close
return trigger_client_event(response, "modal:close", after="swap")