abidibo.net

How to implement modal popup django forms with bootstrap

django forms bootstrap

UPDATE 11/18/2015

I wrote another article like this, but newer, just tested and with support for both bootstrap 3 and 4-alpha, check it out:

http://www.abidibo.net/blog/2015/11/18/modal-django-forms-bootstrap-4/


Sometimes could be a good idea to have the user provide all the needed information without living the "index" or "main" page of our web application. In other terms, sometimes could be a good idea to have some forms appear on a layer, a modal, a popup over the document.

It's quite easy to render a form in a modal window, just make an ajax request and display the result inside your modal.

But things can be a bit trickier when we have to process the form. In particular what happens if any error occurs? Normally django would redirect the user to the form page displaying messages about the errors occurred. But if the form doesn't have its own page (its own complete template), and lives in a modal, how can we manage such situation?

Here we'll see a way to manage all such things using django, bootstrap and its modals, jquery and this plugin.

Scenario

We have a list of items with edit buttons. When clicking the edit button of an item, a form is rendered in a modal, providing an interface to update the model.

The view

We use the django ListView and the UpdateView classes, the first to manage the items list and the second to manage the item update.

from django.views.generic import UpdateView, ListView
from django.http import HttpResponse
from django.template.loader import render_to_string
from myapp.models import Item
from myapp.forms import ItemForm

"""
items list
"""
class ItemListView(ListView):
    model = Item
    template_name = 'myapp/item_list.html'

    def get_queryset(self):
        return Item.objects.all()

"""
Edit item
"""
class ItemUpdateView(UpdateView):
    model = Item
    form_class = ItemForm
    template_name = 'myapp/item_edit_form.html'

    def dispatch(self, *args, **kwargs):
        self.item_id = kwargs['pk']
        return super(ItemUpdateView, self).dispatch(*args, **kwargs)

    def form_valid(self, form):
        form.save()
        item = Item.objects.get(id=self.item_id)
        return HttpResponse(render_to_string('myapp/item_edit_form_success.html', {'item': item}))

Nothing special here.

The ItemListView class is straightforward. While in the ItemUpdateView class I've overwritten the dispatch method in order to get the item id from url, so that I can pass the Item object to the item_edit_form_success template. The item_edit_form template is the one that contains the modal content, nothing more, nothing less.

In your urls.py you must call the ItemUpdateView capturing the pk parameter, which is the id of the item to be edited.

The ItemForm has nothing special so I omit its definition.

The templates

Let's start considering the list template, item_list.html.

{% extends 'base_site.html' %}

{% block extra_js%}
    <script src="http://malsup.github.com/jquery.form.js"></script>
{% endblock %}

{% block content %}
<section>
    <h1>Items list</h1>
    <!-- Modal -->
    <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    </div><!-- /.modal -->

    <table class="table table-bordered table-hovered "cellspacing='0'>
        <tr>
            <th>Item</th>
            <th>Actions</th>
        </tr>
        {% for loan in object_list %}
            <tr>
                <td>{{ item.name }}</td>
                <td>
                    <a class="fa fa-pencil" data-toggle="modal" href="{% url 'item_edit' item.id %}" data-target="#modal" title="edit item" data-tooltip></a> |
                </td>
            </tr>
        {% endfor %}
    </table>
</section>
{% endblock %}

Three things to notice here:

  1. we include the js plugin
  2. we write the markup of the modal container (why? Read about bootstrap modals)
  3. we use a link which will load the response of the given href attribute url inside the model (ajax request)

Read about bootstrap modals to understand the point 3, the main part is:

If a remote URL is provided, content will be loaded one time via jQuery's load method and injected into the .modal-content div. If you're using the data-api, you may alternatively use the href attribute to specify the remote source

Ok so how is made such response? Here comes the template, item_edit_form.html:

<div class="modal-dialog modal-lg">
    <div class="modal-content">
        <form id="item_update_form" method='post' class="form" role="form" action='{% url 'item_edit' item.id %}'>
              <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                <h4 class="modal-title" id="myModalLabel">Item {{ item.id }}</h4>
              </div>
              <div class="modal-body">
                    {% csrf_token %}
                    {{ form.non_field_errors }}
                    <div class="form-group">
                    {% for field in form %}
                        <div class="form-group">
                            {% if field.errors %}
                                <ul class="form-errors">
                                {% for error in field.errors %}
                                    <li><span class="fa fa-exclamation-triangle"></span> <strong>{{ error|escape }}</strong></li>
                                {% endfor %}
                                </ul>
                            {% endif %}
                            {{ field.label_tag }} {{ field }} 
                            {% if field.help_text %}<div class="form-helptext">{{ field.help_text }}</div>{% endif %}
                        </div>
                    {% endfor %}
              </div>
              <div class="modal-footer">
                <input type="button" class="btn btn-default" data-dismiss="modal" value="annulla" />
                <input type="submit" class="btn btn-primary" value="save" style="margin-bottom: 5px;" />
              </div>
        </form>
        <script>
            jQuery('.modal-content .calendar').datepicker({ dateFormat: "yy-mm-dd" });

                var form_options = {
                    target: '#modal',
                    success: function() {  }
                }
                $('#item_update_form').ajaxForm(form_options);
        </script>
    </div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

Some other points to consider:

  1. We give the form an id attribute
  2. We explicitly write the form action. Why? Because the form is loaded inside the list page! So an empty action attribute means that the action url will be the list view url, and that is not good.
  3. We use the provided js code to submit the form through ajax!

In particular the third point is quite important. One of the greatest problems to face when implementing a modal form with django is that if any error occurs we should return in the form page. But if the form hasn't its own page what can we do?

You could think about something like returning in the list page, passing some GET parameter,  read it and consequentially re-open the modal, but the form errors variables would be lost! So you should implement the form action in the list view and pass the form processing result in some way to the ajax url which renders the form.

A simpler and more elegant solution is to submit the form through ajax and capture the response. Then if the response is the same form with a list of errors, we update the content of the modal with it, if the response is a succesfull one we just close the modal; easy!

If you remember, the id attribute of the modal container was #modal, and it is also the target property of the form_options js object!

So we're done, if an error occurs, the form with the error messages is displayed inside the modal, without page reloading.

We have only one another thing to do: write the template which will be used to render the response when the form is submitted without errors, the item_edit_form_success.html, here it is:

<div class="modal-dialog modal-lg">
    <div class="modal-content">
              <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                <h4 class="modal-title" id="myModalLabel">Pratica {{ loan.id }}</h4>
              </div>
              <div class="modal-body">
                  <p>Fuck yeah!</p>
                  <script>
                    setTimeout(function() { jQuery('#modal').modal('hide'); }, 1000);
                    $('body').on('hidden.bs.modal', '.modal', function () {
                        $(this).removeData('bs.modal');
                    });
                  </script>
              </div>
              <div class="modal-footer">
              </div>
    </div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

We display a graceful message and then after one second we destroy the modal. I've said destroy (and not hide), because if we only hide it then a future click on the edit button will cause the same content to open inside the modal (because of how the bootstrap modal works). For this reason we have to destroy it so that the next time it is called, it is re-created.

Summary

It is possible to implement modal forms in django using bootstrap modals and the jquery form plugin. The goal is to have the form submitted by ajax, and then load the ajax response inside the same modal. If an error occurs the form is overwritten with the error messages, otherwise a response is provided which runs a js code which destroys the modal after one second.

Let me know what do you think about this technique!

Edit 2015-02-13

Since this post has raised a bit of interest and I was asked for, I've hosted on github the django app which I used to develop such concepts: https://github.com/abidibo/aidsbank

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured