Ajax form submission with Django and Bootstrap Modals ¶
The purpose of this snippet is to render a Django form either as modal window or standalone page, with the following requirements:
- minimize code repetition
- update the modal with any form errors, then close the modal on successful submission
- process the form normally in the standalone page
Prepare the modal template to contain the form
In this example, the modal is styled for Bootstrap.
Note that the modal body is initially empty; it will be loaded dynamically just before showing it.
file 'templates/frontend/modals/register_edit.html'
{% load i18n %} <div class="modal inmodal" id="modal_register_edit" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content animated bounceInRight"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span> <span class="sr-only">{% trans 'Close' %}</span> </button> <h4 class="modal-title"><i class="fa fa-laptop modal-icon"></i> {% trans 'Register edit' %}</h4> <small class="font-bold">{% trans 'Insert new values to modify Regiser attributes as required' %}</small> </div> <div class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="btn btn-white" data-dismiss="modal">{% trans 'Close' %}</button> <button id="btn-save" type="button" class="btn btn-primary">{% trans 'Save changes' %}</button> </div> </div> </div> </div>
Display the modal
The modal can be invoked via javascript, for example from a link, passing the event and any other required parameters to the handler:
<a href="{% url 'frontend:register-edit' register.id %}" onclick="openRegisterEditModal(event, '{{register.id}}'); return false;"> {% trans 'Edit' %} </a>
Note that we also assigned to the "href" attribute the end-point in charge for the form rendering; we'll retrieve it later in the handler.
In the handler, we first update the modal's body with the HTML loaded asynchronously from the server, and then (after the ajax call) display it.
file: 'static/frontend/js/register_edit.js'
function openRegisterEditModal(event, register_id) { var modal = $('#modal_register_edit'); var url = $(event.target).closest('a').attr('href'); modal.find('.modal-body').html('').load(url, function() { modal.modal('show'); formAjaxSubmit(popup, url); }); }
Please note that we clear the modal body - html('') - before calling load(), to prevent showing an obsolete content in case of load failure.
The role of the formAjaxSubmit() helper is explained below.
Eventually, you might desire more control over load failure, for example if the server refuses to provide the form for inconsistent user permissions:
@login_required def register_edit(request, pk): if not request.user.has_perm('backend.change_register'): raise PermissionDenied ...
If this is the case, use $.ajax() instead of load:
function openRegisterEditModal(event, register_id) { var modal = prepare_generic_modal(element); var url = $(event.target).closest('a').attr('href'); $.ajax({ type: "GET", url: url }).done(function(data, textStatus, jqXHR) { modal.find('.modal-body').html(data); modal.modal('show'); formAjaxSubmit(modal, url, updateRegisterCalibrationChart); }).fail(function(jqXHR, textStatus, errorThrown) { alert(errorThrown); }); }
Form validation in the modal
If the modal body contains a form, we need to prevent it from performing its default submit action.
We do this binding to the form's submit event, where we serialize the form's content and sent it via an Ajax call using the form’s defined method.
If the form specifies an action, we use it as end-point of the ajax call; if not, we're using the same view for both rendering and form processing, so we can reuse the original url instead.
Upon a successufull response from the server, we need to further investigate the HTML received: if it contains any field errors, the form did not validate successfully, so we update the modal body with the new form and its error; otherwise, the modal will be closed.
file: 'static/frontent/js/utils.js'
'use strict'; function formAjaxSubmit(modal, action) { var form = modal.find('.modal-body form'); var footer = $(modal).find('.modal-footer'); // bind to the form’s submit event $(form).on('submit', function(event) { // prevent the form from performing its default submit action event.preventDefault(); footer.addClass('loading'); // either use the action supplied by the form, or the original rendering url var url = $(this).attr('action') || action; // serialize the form’s content and sent via an AJAX call // using the form’s defined method $.ajax({ type: $(this).attr('method'), url: url, data: $(this).serialize(), success: function(xhr, ajaxOptions, thrownError) { // If the server sends back a successful response, // we need to further check the HTML received // If xhr contains any field errors, the form did not // validate successfully, so we update the modal body // with the new form and its error if ($(xhr).find('.has-error').length > 0) { $(modal).find('.modal-body').html(xhr); formAjaxSubmit(modal, url); } else { // otherwise, we've done and can close the modal $(modal).modal('hide'); } }, error: function(xhr, ajaxOptions, thrownError) { }, complete: function() { footer.removeClass('loading'); } }); }); }
/* Add spinner while new image gets loaded completely: https://stackoverflow.com/questions/21258195/add-spinner-while-new-image-gets-loaded-completely#21262404 Set the CSS background-image property of the images to a loading spinner graphic; once the actual image is downloaded, it covers up the "loading" animated GIF background image. */ .loading { background: transparent url(/static/frontend/images/spinner.gif) no-repeat scroll center center; }
Server side processing
The view responsible to process the form server-side has no special requirements, except that we want to use it for both modals and as a full standalone page.
We obtain this by splitting the rendering template as follows, and using the "inner" version whenever we detect an ajax request.
file: 'templates/frontend/includes/register_edit_form.html'
{% extends "base.html" %} {% load static staticfiles i18n %} {% block content %} {% include 'frontend/includes/register_edit_form_inner.html' %} {% endblock content %}
file: 'templates/frontend/includes/register_edit_form_inner.html'
{% load i18n bootstrap3 %} <div class="row"> <div class="col-sm-4"> <form action="{% url 'frontend:register-edit' register.id %}" method="post" class="form"> {% csrf_token %} {% bootstrap_form form %} {% buttons %} <div class="form-submit-row"> <button type="submit" class="btn btn-primary"> {% bootstrap_icon "star" %} {% trans 'Send' %} </button> </div> {% endbuttons %} </form> </div> </div>
As explained above, the form action is optional in this context.
And finally, here's the view code:
file: 'urls.py'
urlpatterns = [ ... path('register/<uuid:pk>/edit', views.register_edit, name="register-edit"),
file: 'views.py'
from django.shortcuts import render from .forms import RegisterEditForm def register_edit(request, pk): # Either render only the modal content, or a full standalone page if request.is_ajax(): template_name = 'frontend/includes/register_edit_form_inner.html' else: template_name = 'frontend/includes/register_edit_form.html' register = get_object_by_uuid_or_404(Register, pk) if request.method == 'POST': form = RegisterEditForm(instance=register, data=request.POST) if form.is_valid(): form.save() else: form = RegisterEditForm(instance=register) return render(request, template_name, { 'register': register, 'form': form, })
Use a single modal for different purposes
Provided you have a suitable template for a generic modal:
{% load i18n %} <div class="modal inmodal" id="modal_generic" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content animated bounceInRight"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span> <span class="sr-only">{% trans 'Close' %}</span> </button> <h4><i class="fa fa-laptop modal-icon"></i> <span class="modal-title"></span></h4> <small class="modal-subtitle font-bold"></small> </div> <div class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="btn btn-white" data-dismiss="modal">{% trans 'Close' %}</button> <button id="btn-save" type="button" class="btn btn-primary">{% trans 'Save changes' %}</button> </div> </div> </div> </div>
you might want to specify a custom title and subtitle in the calling template (where strings can be easily localized):
<a href="{% url 'frontend:register-set_value' register.id %}" data-title="{% trans 'Register set value' %}" data-subtitle="{% trans 'Insert the new value to be assigned to the Register' %}" onclick="openRegisterSetValueModal(event, '{{register.id}}'); return false;"> <i class="fa fa-keyboard-o"></i> {{ register.description }} </a>
and set the appropriate values from javascript before showing the modal:
function openRegisterSetValueModal(event, register_id) { var modal = $('#modal_generic'); var element = $(event.target).closest('a'); modal.find('.modal-title').text(element.data('title')); modal.find('.modal-subtitle').text(element.data('subtitle')); ... }
Take extra action after successful submit
In this use case, we want to update the main HTML page after a modal form has been successfully submitted, to reflect any possible change.
We start by adding a second cbAfterSuccess callback to our formAjaxSubmit() helper:
function formAjaxSubmit(modal, action, cbAfterLoad, cbAfterSuccess) { var form = modal.find('.modal-body form'); var header = $(modal).find('.modal-header'); // use footer save button, if available var btn_save = modal.find('.modal-footer #btn-save'); if (btn_save) { modal.find('.modal-body form .form-submit-row').hide(); btn_save.off().on('click', function(event) { modal.find('.modal-body form').submit(); }); } if (cbAfterLoad) { cbAfterLoad(modal); } setTimeout(function() { modal.find('form input:visible').first().focus(); }, 1000); // bind to the form’s submit event $(form).on('submit', function(event) { // prevent the form from performing its default submit action event.preventDefault(); header.addClass('loading-left'); var url = $(this).attr('action') || action; // serialize the form’s content and sent via an AJAX call // using the form’s defined action and method $.ajax({ type: $(this).attr('method'), url: url, data: $(this).serialize(), success: function(xhr, ajaxOptions, thrownError) { // If the server sends back a successful response, // we need to further check the HTML received // update the modal body with the new form $(modal).find('.modal-body').html(xhr); // If xhr contains any field errors, // the form did not validate successfully, // so we keep it open for further editing if ($(xhr).find('.has-error').length > 0) { formAjaxSubmit(modal, url, cbAfterLoad, cbAfterSuccess); } else { // otherwise, we've done and can close the modal $(modal).modal('hide'); if (cbAfterSuccess) { cbAfterSuccess(modal); } } }, error: function(xhr, ajaxOptions, thrownError) { }, complete: function() { header.removeClass('loading-left'); } }); }); }
When opening the modal, we supply the address of a suitable handler to be invoked after successfull submit; note that we also store some extra data into the modal object, for later reference in afterRegisterEditModalSuccess():
function openRegisterEditModal(event, register_id) { var modal = $('#modal_register_edit'); modal.data('register-id', register_id); modal.data('target', event.target); var element = $(event.target).closest('a'); var url = element.attr('href'); modal.find('.modal-body').load(url, function() { modal.modal('show'); formAjaxSubmit(modal, url, null, afterRegisterEditModalSuccess); }); } function afterRegisterEditModalSuccess(modal) { var url = sprintf('/register/%s/render', modal.data('register-id')); var element = $(modal.data('target').closest('.register.widget')); var container = element.parent(); container.load(url, function() { var new_element = container.find('.register.widget'); init_register_widgets(new_element); }); }