Django And JQuery Ajax, Part 2: Forms

This article is a follow up to an article you can read here about using Django and JQuery to load paginated subpages. That article explains some of the principles that will be used again here. In this article I will show a simple way to integrate Django forms seamlessly into another page using JQuery ajax functions. The integration is kept as straightforward as possible, making sure csrf tokens, validation, error messages, and redirects work just the same way as they do on the original, non Ajax-integrated form.

The Django Form

Let's start by creating a form. Django's CreateView offers a convenient way to create a form to add new objects to a model.

from django.views.generic.edit import CreateView
from django.core.urlresolvers import reverse_lazy

class CreateMyModelView(CreateView):
    model = MyModel
    template_name = 'myapp/mymodel_create_form.html'
    success_url = reverse_lazy("create_mymodel_success")

(Snippet)

The success_url is the URL for the page that the visitor will be redirected to after a successful form submission.

Define the URLs for the form and for the success page in urls.py.

from django.conf.urls import url
from django.views.generic import TemplateView
from .views import CreateMyModelView

urlpatterns = [
    url(r'^create/', CreateMyModelView.as_view(), 
        name='create_mymodel_form'),
    url(r'^congratulations/', 
        TemplateView.as_view(template_name="myapp/create_mymodel_success.html"), 
        name='create_mymodel_success'),
]

(Snippet)

Finally we need to create the templates for the form and the success url. The form template (create_mymodel_form.html) would look like this

<form id="formid" action="{{ request.path }}" method="post">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {{ form.as_p }}
    <button type="submit">Create My Model</button>
</form>

(Snippet)

And the template for the success url (create_mymodel_success.html) can just be any plain HTML page or anything else that makes sense. If MyModel has a get_absolute_url method, the CreateMyModelView class doesn't even need to define a success_url. The default success_url in that case would be the get_absolute_url page of the successfully created object.

That concludes the section about setting up a simple form for the project. The form template is not a full HTML page, because it is missing the <html> and <body> tags. But we can use it just fine for inclusion on another page with our Ajax script. I didn't include example code for MyModel, but any model will do. You can use the example from this project.

The Ajax Script

Before I show the full script, I want to explain the basic structure of how it works. The form is loaded using JQuery's load method to place the form on a target element (#formdiv) on the page. The load method takes an argument, called "complete" in the docs, which is the function that is executed once the loading of the form data is complete. So let's call this function the complete-function. The job of the complete-function is to bind an ajax submit handler, let's call it "handle_ajax_form", to the form. By replacing the default submit behavior with handle_ajax_form, the form submission can be handled quietly in the background, without leading to a complete page refresh. So let's have a look at what the load call would look like.

$("#formdiv").load(
    "myapp/create",
    function(){
        $("#formid").submit(
                handle_ajax_form
          );
    }
);

(Snippet)

The selector "#formid" corresponds to the id we defined in create_mymodel_form.html for the form. The complete-function defined in the load call therefore binds handle_ajax_form to the form, after the form has been inserted into the "#formdiv" element. As a result handle_ajax_form will be executed when the form is submitted by pressing the submitt button. Now we still need to write handle_ajax_form. However we need to make sure that handle_ajax_form knows the form selector ("#formid" in our case), and I will use a closure to pass the form selector into the handle_ajax_form function. We could also attach the formid to the eventData in the submit call, but I find the closure cleaner to read (just because the parameter declaration is more obvious and closer to the function code). And closures are fun too, so here is how that approach works.

function get_ajax_formhandler(formselector){
    function handle_ajax_form(event){
        // write handle_ajax_form code here
        // formselector is available inside this inner function
    }
    return handle_ajax_form;
}

handle_ajax_form = get_ajax_formhandler("#formid");

(Snippet)

The function get_ajax_formhandler returns the handle_ajax_form function, but depending on what formselector was passed into get_ajax_formhandler, that value will be available in the returned handle_ajax_form function. You can read more about closures here.

So what should the handle_ajax_form function do and why does it need access to the form selector? First it needs to define an Ajax call that exactly matches the original form submission behavior. Once the form has been submitted as an Ajax call, it has to handle the response that the server returns to it. Assuming the server doesn't return a HTTP error code, there are two possible responses we have to handle. Either the submission was a success and the server returns the success url defined in CreateMyModelView.success_url, or the submission was a failure and the server returns the form with error messages. In that second case, the failure case, the form needs to be replaced on the page with the new form data received by the server. And furthermore since the newly inserted form doesn't have the submit bindings of the previous one yet, the handle_ajax_form must be bound to the form again. So here is the code for that.

function get_ajax_form_handler(formselector){
    function handle_ajax_form(event){
        $.ajax({
            async: true,
            type: $(formselector).attr("method"),
            url: $(formselector).attr("action"),
            data: $(formselector).serialize(),
            success: function(data){
                        formdata = $(data).filter(formselector);
                        
                        if(formdata.length){ 
                            // the returned data contained the form
                            // so we have to reinsert it and bind to function again
                            $(formselector).replaceWith(formdata);
                            $(formselector).submit(handle_ajax_form);
                        }else{ 
                            // there was no form, so assume the submit was a success
                            // and just insert the data over the form
                            $(formselector).replaceWith(data);
                        }
                    }
        });
        return false;// this suppresses the default action for the form submit
                     // so only handle_ajax_form gets executed and nothing else
    }
    return handle_ajax_form;
}

(Snippet)

And that is it. The success function handles the case that the server returned the form, in the "if(formdata.length)"-branch by refreshing and rebinding the form. "formdata" would have zero length if the form wasn't found in the response data. This case, which means that the success url was returned, is handled in the else-branch by overriding the form with the received data. The form is thereby removed from the page.

The Template Tag

At this point we have everything in place to use forms like the one in CreateMyModelView as a part of any other page. All we need to do is to place a formdiv element on the page.

<div id="formdiv"></div>

And then load the form into that element as follows.

$("#formdiv").load(
    "myapp/create",
    function(){
        $("#formid").submit(
                get_ajax_formhandler("#formid")
          );
    }
);

The last step is to write an assignment template tag, that will eliminate the need to redundantly copy and paste these two code segments above onto each page or template where it is used. In the previous Django and Ajax related article, which you can read here, I explained why I prefer to use assignment template tags over template includes for that purpose. This is the template tag code for the example at hand.

from django import template

register = template.Library()

@register.assignment_tag
def get_postform(formurl, *args, **kwargs):
    formdiv_id = kwargs.get("formdiv_id", "formdiv-id")
    formselector = kwargs.get("formselector", "#formid")
    
    loadscript = """
                    $('#{formdiv_id}').load(
                        {formurl},
                        function(){
                         $('{formselector}').submit(
                               get_ajax_formhandler('{formselector}')
                          );
                      }
                    );
                """.format(
                        formurl = formurl,
                        formdiv_id = formdiv_id,
                        formselector = formselector,
                    )
    
    return {
            "formdiv": "<div id='{formdiv_id}'></div>".format(
                                            formdiv_id = formdiv_id),
            "loadscript": loadscript,
        }

(Snippet)

The code above must be placed in a file inside the subdirectory templatetags/ of the app, for example in templatetags/ajax.py, and a file __init__.py must be placed inside templatetags/ too. Given these prerequisites are fulfilled, the get_postform template tag can be accessed and the dictionary that it returns can be used inside the templates in the following way.

{% load ajax %}
{% url "create_mymodel_form" as formurl %}
{% get_postform formurl as postform %}

<html>
   <body>
      {{ postform.formdiv|safe }}
      
      <!--
         // Don't forget to include jquery and ajax.js 
         // in that order here
      -->
      <script>
         $(function() {
            {{ postform.loadscript|safe }}
         });
      </script>
   </body>
</html>

(Snippet)

In short what this template does is to load the template tags library (ajax.py), then assign the dictionary for the "create_mymodel_form" form to the postform variable, and finally the formdiv and loadscript values from that dictionary are placed in the appropriate places on the page. The loadscript is bound to the document ready function, so the form will be automatically inserted into the page for the first time, when the page is ready.

 

 



Comments



Post A Comment