Using django and htmx I need to do the following. In a parent form create-update.html
I need to give a user the
ability to add another 'contact' instance to a field that contains a drop list of all current 'contact'
instances in the database. I want the user to click an add button next to the sro.form
field, which renders a modal
containing a child form new_contact.html
. The user will then enter the new 'contact' instance into the child
form in the modal form new_contact.html
and click save. This will cause the new contact instance to be saved to
the database via create_contact_view()
, and for the the sro
form field in the parent form create-update.html
to be replaced via an AJAX call, with an refreshed dropdown list I've partially completed this. I have a working
modal form which is saving new 'contact' instances to the database, but the sro.form
field just disappears and isn't
reloaded. Here's my code. Many thanks.
views.py
class ProjectCreateView(CreateView):
model = Project
form_class = ProjectUpdateForm
template_name = "create-update.html"
def get_context_data(self, **kwargs):
context = super(ProjectCreateView, self).get_context_data(**kwargs)
context["type"] = "Project"
context["new_contact_url"] = reverse("register:new-contact") # to create new url
return context
def form_valid(self, form, *args, **kwargs):
mode = form.cleaned_data["mode"]
if mode is None:
form.add_error("mode", "This field is required")
return self.form_invalid(form)
return super().form_valid(form)
def create_contact_view(request):
form = ContactForm(request.POST or None)
url = reverse("register:new-contact") # to create method
context = {
"form": form,
"url": url,
}
if form.is_valid():
form.save()
context['form'] = ProjectUpdateForm(request.POST or None)
return render(request, "partials/hx_new_contact.html", context)
return render(request, "new_contact.html", context)
create-update.html
<div class="form-group">
<form method="POST" novalidate> {% csrf_token %}
{{ form.name|as_crispy_field }}
{% if not form.instance.pk %}
{{ form.mode|as_crispy_field}}
{% endif %}
{{ form.gov_tier|as_crispy_field }}
{{ form.group|as_crispy_field }}
{% include 'partials/hx_new_contact.html' %}
{{ form.pd|as_crispy_field }}
{{ form.pmo|as_crispy_field }}
{{ form.description|as_crispy_field }}
<br><input class="btn btn-primary" type="submit" value="Save"/>
</form>
</div>
partials/hx_new_contact.html
{% load crispy_forms_tags %}
<div id="modals-here">
{{ form.sro|as_crispy_field }}
<button
hx-get="{{ new_contact_url }}"
hx-target="#modals-here"
hx-trigger="click"
hx-swap="innerHTML"
class="btn btn-primary"
_="on htmx:afterOnLoad wait 10ms then add .show to #modal then add .show to
#modal-backdrop">Add Contact</button>
</div>
new_contact.html
<div id="modal-backdrop" class="modal-backdrop fade show" style="display:block;"></div>
<div id="modal" class="modal fade show" tabindex="-1" style="display:block;">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Contact</h5>
</div>
<div class="modal-body">
<form action="." method="POST" hx-post="{% if url %}{{ url }}{% else %}.{% endif %}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary" onclick="closeModal()">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Close</button>
</form>
</div>
</div>
</div>
</div>
<script>
function closeModal() {
var container = document.getElementById("modals-here")
var backdrop = document.getElementById("modal-backdrop")
var modal = document.getElementById("modal")
modal.classList.remove("show")
backdrop.classList.remove("show")
setTimeout(function() {
container.removeChild(backdrop)
container.removeChild(modal)
}, 200)
}
</script>
First your modal container should be somewhere at the end of your body
, not in your form, so move that div to the bottom of the page.
{% load crispy_forms_tags %}
<div id="contact-input-container"> <!-- make a <div id="modals-here"></div> at the end of the <body> and rename this div #contact-input-container -->
{{ form.sro|as_crispy_field }}
<button
hx-get="{{ new_contact_url }}"
hx-target="#modals-here"
hx-trigger="click"
hx-swap="innerHTML"
class="btn btn-primary"
_="on htmx:afterOnLoad wait 10ms then add .show to #modal then add .show to
#modal-backdrop">Add Contact
</button>
</div>
In your this modal, you forgot to set the hx-target
and hx-swap
attributes, so the form replaced itself in your example, then you hide the modal, that's why you don't see any change.
<div id="modal-backdrop" class="modal-backdrop fade show" style="display:block;"></div>
<div id="modal" class="modal fade show" tabindex="-1" style="display:block;">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Contact</h5>
</div>
<div class="modal-body">
<!-- ADD hx-target and hx-swap attributes to your form -->
<form
action="."
method="POST"
hx-post="{% if url %}{{ url }}{% else %}.{% endif %}"
hx-target="#contact-input-container"
hx-swap="outerHTML"
>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary" onclick="closeModal()">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Close</button>
</form>
</div>
</div>
</div>
</div>
<script>
function closeModal() {
var container = document.getElementById("modals-here")
var backdrop = document.getElementById("modal-backdrop")
var modal = document.getElementById("modal")
modal.classList.remove("show")
backdrop.classList.remove("show")
setTimeout(function() {
container.removeChild(backdrop)
container.removeChild(modal)
}, 200)
}
</script>