Derek Morgan bio photo

Derek Morgan

A programmer living in Baltimore, Maryland.

Email Twitter LinkedIn Github

Twitter Bootstrap contains an exhaustive set of CSS classes, components, and jQuery plugins that are responsive across a broad array of devices. When combined with the Django web framework, creating responsive, data-driven websites becomes quick and easy for Python developers.

The modal dialog box is one of the components Bootstrap provides. Since Django’s class-based views makes it easy to define templates and forms with complex validation rules, using Django to generate the contents of these modal dialog boxes has become a common task.

A typical Bootstrap modal dialog box

Many different methods exist for accomplishing this task, but these solutions often require duplicating template code, don’t address rendering the same view as both a standalone page and the contents of a modal, or don’t account for redirects when submitting forms.

I recently found myself trying to render a Django view as both a modal and a standalone page and had the following requirements:

  • minimize code repetition
  • update the modal with any form errors
  • close the modal on successful submission

I was able to accomplish this task with a few lines of Python, a few lines of JavaScript, and a minor template change.

Server-Side Changes

The server-side changes consisted of creating an AjaxTemplateMixin to render a different template for AJAX requests and making a small change to an existing template.

An AjaxTemplateMixin

 1 class AjaxTemplateMixin(object):
 2 
 3     def dispatch(self, request, *args, **kwargs):
 4         if not hasattr(self, 'ajax_template_name'):
 5             split = self.template_name.split('.html')
 6             split[-1] = '_inner'
 7             split.append('.html')
 8             self.ajax_template_name = ''.join(split)
 9         if request.is_ajax():
10             self.template_name = self.ajax_template_name
11         return super(AjaxTemplateMixin, self).dispatch(request, *args, **kwargs)

The first step required writing a mixin to add an ajax_template_name attribute Django’s class-based views. If this attribute is not explicitly defined, it will default to adding _inner to the end of the template_name attribute. For example, take the following FormView class:

1 class TestFormView(SuccessMessageMixin, AjaxTemplateMixin, FormView):
2     template_name = 'test_app/test_form.html'
3     form_class = TestForm
4     success_url = reverse_lazy('home')
5     success_message = "Way to go!"

In this example, the ajax_template_name defaults to test_app/test_form_inner.html. If the request is AJAX, then the view renders this template. Otherwise, the view renders the test_app/test_form.html template.

Create the AJAX Template

Now that the view will render ajax_template_name for AJAX requests we have to create it. This template could be unique, but more than likely it will be the same as template_name but without extending the base template containing the site’s header, navigation, and footer. This could be as simple as changing test_app/test_form.html from:

 1 {% extends 'test_app/home.html' %}
 2 
 3 {% block content %}
 4 {% load crispy_forms_tags %}
 5 <div class="row">
 6     <form class="form-horizontal" action="{% url 'test-form' %}" method="post">
 7         {% crispy form %}
 8         <input type="submit" class="btn btn-submit col-md-offset-2">
 9     </form>
10 </div>
11 {% endblock content %}

to:

1 {% extends 'test_app/home.html' %}
2 
3 {% block content %}
4 {% include 'test_app/test_form_inner.html' %}
5 {% endblock content %}

and creating test_app/test_form_inner.html containing:

1 {% load crispy_forms_tags %}
2 <div class="row">
3     <form class="form-horizontal" action="{% url 'test-form' %}" method="post">
4         {% crispy form %}
5         <input type="submit" class="btn btn-submit col-md-offset-2">
6     </form>
7 </div>

All we’ve done here is moved the HTML within the content block to its own template. The example template uses django-crispy-forms to generate the form markup using Bootstrap CSS classes but this is not a requirement.

Front-end Changes

At this point, rendering your view is easy, unless it contains a form.

Rendering a View in a Modal the Easy Way

Given the following modal in your HTML:

 1 <div class="modal fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
 2   <div class="modal-dialog">
 3     <div class="modal-content">
 4       <div class="modal-header">
 5         <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
 6         <h4 class="modal-title">Modal title</h4>
 7       </div>
 8       <div id="form-modal-body" class="modal-body">
 9         ...
10       </div>
11       <div class="modal-footer">
12         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
13       </div>
14     </div>
15   </div>
16 </div>

rendering a Django view in it can be as simple as adding:

1 <a data-toggle="modal" href="{% url 'test-form' %}" data-target="#form-modal">Click me</a>

to your template. Behind the scenes, Bootstrap is using the data attributes to call jQuery’s .load() method to make an AJAX call to the test-form url and replace the HTML within #form-modal. However, there are a couple problems with this:

  • Using data attributes replaces the entire contents of the modal, so your template will need to contain the .modal-dialog, .modal-content and .modal-body DIVs to render properly.
  • jQuery’s .load() is only called once the first time the modal is opened.
  • Any redirects that occur, such as from submitting a form, will redirect the entire page.

If none of this matters to you, then great, you’re done! Otherwise, keep reading.

Rendering a View in a Modal the Slightly Harder Way

The following JavaScript solves the problems above:

 1 var formAjaxSubmit = function(form, modal) {
 2     $(form).submit(function (e) {
 3         e.preventDefault();
 4         $.ajax({
 5             type: $(this).attr('method'),
 6             url: $(this).attr('action'),
 7             data: $(this).serialize(),
 8             success: function (xhr, ajaxOptions, thrownError) {
 9                 if ( $(xhr).find('.has-error').length > 0 ) {
10                     $(modal).find('.modal-body').html(xhr);
11                     formAjaxSubmit(form, modal);
12                 } else {
13                     $(modal).modal('toggle');
14                 }
15             },
16             error: function (xhr, ajaxOptions, thrownError) {
17                 // handle response errors here
18             }
19         });
20     });
21 }
22 $('#comment-button').click(function() {
23     $('#form-modal-body').load('/test-form/', function () {
24         $('#form-modal').modal('toggle');
25         formAjaxSubmit('#form-modal-body form', '#form-modal');
26     });
27 }

This code binds to the click event on #comment-button and loads the /test-form/ HTML asynchronously into the body of the modal. Since this is an AJAX call, the test_form/test_form_inner.html template will be rendered and the form will be displayed without any site navigation or footer.

Additionally, this code also calls formAjaxSubmit(). This function binds to the form’s submit event. By calling preventDefault(), the callback function prevents the form from performing its default submit action. Instead, the form’s content is serialized and sent via an AJAX call using the form’s defined action and method.

If the server sends back a successful response, the success function is called. The xhr parameter contains the HTML received from the server. Note that a successful response from the server does not mean that the form validated successfully. Therefore, xhr is checked to see if it contains any field errors by looking for the has_error Bootstrap class in its contents. If any errors are found, the modal’s body is updated with the form and its errors. Otherwise, the modal is closed.

A form with errors in a Bootstrap modal dialog box

Summary

In conclusion, we were able to define our form and view in Django and render it correctly in a Bootstrap modal by adding one short Django mixin, making a small change to an existing template, and adding a few lines of JavaScript.

The example site used in this post is hosted on GitHub.