Search code examples
djangohtmx

Django and HTMX for dynamically updating dropdown list in form.field


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>

Solution

  • 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.

    hx_new_contact.html
    {% 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>
    

    new_contact.html

    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>