trco / django-bootstrap-modal-forms

A Django plugin for creating AJAX driven forms in Bootstrap modal.
MIT License
383 stars 142 forks source link

Adding callback function to Async Update #144

Open kpdebree opened 3 years ago

kpdebree commented 3 years ago

I'm having an issue with the asynchronous ajax updates. I have a table that is rendered using datatables.js, and I want to have an ajax create button that adds new items to the table. I can successfully create the items asynchronously, but it kills the datatable, which is rendered in Javascript.

When the form is submitted, I want to be able to add in a callback that rerenders the table.

Additionally, as a separate issue, there's some problems with closing the modal on submit.

trco commented 3 years ago

@kpdebree Following line in .js of django-bootstrap-modal-forms should rerender your table https://github.com/trco/django-bootstrap-modal-forms/blob/master/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js#L89. Element with dataElementId should be filled with string you prepare with separate view like in package examples https://github.com/trco/django-bootstrap-modal-forms/blob/master/examples/views.py#L99.

Have you already tried to implement callback in .js file included in this package? I believe this shouldn't be hard at all. I would approach it his way (1) add callback field to asyncSettings here https://github.com/trco/django-bootstrap-modal-forms/blob/master/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js#L154, which will take your custom function and then (2) run this callback function instead of default update in this place https://github.com/trco/django-bootstrap-modal-forms/blob/master/bootstrap_modal_forms/static/js/jquery.bootstrap.modal.forms.js#L88.

I would be interested in including this if you can confirm it and maybe share some code. Unfortunately I don't have enough time at the moment to start implementing it from scratch.

PritamDutt commented 3 years ago

This is what I did (my quick and dirty work), as I also needed capability to reload my datatable whenever a new record was added.

  1. Modified the original code to allow passing a callback function
  2. asyncSettings.fnPostUpdateRefresh to be precise
  3. Moved the native functionality into a separate function postUpdateRefresh
  4. Other Key changes:
    • validateAsyncSettings now ignores other settings if fnPostUpdateRefresh is defined
    • model is always closed before making call to fnPostUpdateRefresh
    • cosmetic change .. now submit shows a spinner while ajax queries are being performed.. gives user a sense that something is happening.. as I could take some time with large forms / slow response

One of the key reason for refactoring code was avoiding following line, as that is something I surely don't want in my use case.

$(asyncSettings.dataElementId).html(response[asyncSettings.dataKey]);

I have added this file to my static folder, which overrides the file used by library.

I am sure there can be better implementation of it.. but this is my 1st cut.. and I thought to share it..


/*
django-bootstrap-modal-forms
version : 2.0.1
Copyright (c) 2020 Uros Trstenjak
https://github.com/trco/django-bootstrap-modal-forms
*/

;(function ($) {
    "use strict";

    // Open modal & load the form at formURL to the modalContent element
    var modalForm = function (settings) {
        $(settings.modalID).find(settings.modalContent).load(settings.formURL, function () {
            $(settings.modalID).modal("show");
            $(settings.modalForm).attr("action", settings.formURL);
            addEventHandlers(settings);
        });
    };

    var addEventHandlers = function (settings) {
        // submitBtn click handler
        $(settings.submitBtn).on("click", function (event) {
            isFormValid(settings, submitForm);
        });
        // Modal close handler
        $(settings.modalID).on("hidden.bs.modal", function (event) {
            $(settings.modalForm).remove();
        });
    };

    // Check if form.is_valid() & either show errors or submit it via callback
    var isFormValid = function (settings, callback) {
        $.ajax({
            type: $(settings.modalForm).attr("method"),
            url: $(settings.modalForm).attr("action"),
            data: new FormData($(settings.modalForm)[0]),
            contentType: false,
            processData: false,
            beforeSend: function () {
                let spinnerHtml = "<i style='margin-left: 5px' class='fa fa-spinner fa-spin'/>";
                let btn = $(settings.submitBtn);
                btn.prop("disabled", true);
                // Display spinner to denote action is underway
                btn.data('inner', btn.html());
                btn.html(btn.html() + spinnerHtml);
            },
            success: function (response) {
                if ($(response).find(settings.errorClass).length > 0) {
                    // Form is not valid, update it with errors
                    $(settings.modalID).find(settings.modalContent).html(response);
                    $(settings.modalForm).attr("action", settings.formURL);
                    // Reinstantiate handlers
                    addEventHandlers(settings);
                } else {
                    // Form is valid, submit it
                    callback(settings);
                }
            }
        });
    };

    var postUpdateRefresh = function () {
        $.ajax({
            type: "GET",
            url: asyncSettings.dataUrl,
            dataType: "json",
            success: function (response) {
                // Update page
                $(asyncSettings.dataElementId).html(response[asyncSettings.dataKey]);

                // Add modalForm to trigger element after async page update
                if (asyncSettings.addModalFormFunction) {
                    asyncSettings.addModalFormFunction();
                }

                if (asyncSettings.closeOnSubmit) {
                    $(settings.modalID).modal("hide");
                } else {
                    // Reload form
                    $(settings.modalID).find(settings.modalContent).load(settings.formURL, function () {
                        $(settings.modalForm).attr("action", settings.formURL);
                        addEventHandlers(settings);
                    });
                }
            }
        });

    }
    // Submit form callback function
    var submitForm = function (settings) {
        if (!settings.asyncUpdate) {
            $(settings.modalForm).submit();
        } else {
            var asyncSettingsValid = validateAsyncSettings(settings.asyncSettings);
            var asyncSettings = settings.asyncSettings;

            if (asyncSettingsValid) {
                var formdata = new FormData($(settings.modalForm)[0]);
                // Add asyncUpdate and check for it in save method of CreateUpdateAjaxMixin
                formdata.append("asyncUpdate", "True");
                $.ajax({
                    type: $(settings.modalForm).attr("method"),
                    url: $(settings.modalForm).attr("action"),
                    data: formdata,
                    contentType: false,
                    processData: false,
                    success: function (response) {
                        var body = $("body");
                        if (body.length === 0) {
                            console.error("django-bootstrap-modal-forms: <body> element missing in your html.");
                        }
                        body.prepend(asyncSettings.successMessage);
                        // Update page without refresh
                        if (asyncSettings.fnPostUpdateRefresh) {
                            // dismiss modal
                            $(settings.modalID).modal("hide");
                            //Call custom function if defined
                            asyncSettings.fnPostUpdateRefresh();
                        } else {
                            postUpdateRefresh();
                        }
                    }
                });
            }
        }
    };

    var validateAsyncSettings = function (settings) {
        var missingSettings = [];

        if (settings.fnPostUpdateRefresh && typeof (settings.fnPostUpdateRefresh) === "function") {
            // Ignore other settings in case of user provider post-op function
            // and replace the standard function with current function
            postUpdateRefresh = settings.fnPostUpdateRefresh
            return true;
        }
        if (!settings.successMessage) {
            missingSettings.push("successMessage");
            console.error("django-bootstrap-modal-forms: 'successMessage' in asyncSettings is missing.");
        }
        if (!settings.dataUrl) {
            missingSettings.push("dataUrl");
            console.error("django-bootstrap-modal-forms: 'dataUrl' in asyncSettings is missing.");
        }
        if (!settings.dataElementId) {
            missingSettings.push("dataElementId");
            console.error("django-bootstrap-modal-forms: 'dataElementId' in asyncSettings is missing.");
        }
        if (!settings.dataKey) {
            missingSettings.push("dataKey");
            console.error("django-bootstrap-modal-forms: 'dataKey' in asyncSettings is missing.");
        }
        if (!settings.addModalFormFunction) {
            missingSettings.push("addModalFormFunction");
            console.error("django-bootstrap-modal-forms: 'addModalFormFunction' in asyncSettings is missing.");
        }

        return missingSettings.length <= 0;

    };

    $.fn.modalForm = function (options) {
        // Default settings
        var defaults = {
            modalID: "#modal",
            modalContent: ".modal-content",
            modalForm: ".modal-content form",
            formURL: null,
            errorClass: ".invalid",
            submitBtn: ".submit-btn",
            asyncUpdate: false,
            asyncSettings: {
                closeOnSubmit: false,
                successMessage: null,
                dataUrl: null,
                dataElementId: null,
                dataKey: null,
                addModalFormFunction: null,
                fnPostUpdateRefresh: null // reference to custom function that should be called after refresh
            }
        };

        // Extend default settings with provided options
        var settings = $.extend(defaults, options);

        this.each(function () {
            // Add click event handler to the element with attached modalForm
            $(this).click(function (event) {
                // Instantiate new form in modal
                modalForm(settings);
            });
        });

        return this;
    };

}(jQuery));
rez0n commented 2 years ago

Hi @kpdebree @trco Do you have an idea how to get created object id in the callback of the async modal?