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.
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">×</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.
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.