Brainstorm's snippets (1/226)

  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">&times;</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">&times;</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);
    });
}