Dynamic List Creation with Django and AJAX

Have you ever been to a site where they have that "+" next to a list of fields or files and wanted to implement that yourself? I was in a situation at work where we wanted to add a bunch of key-value pairs to our database and we didn't know how to do it. As a temporary solution, a team member used formsets with extra=1, but this required a page refresh for every new entry as well as a database hit (see Adding forms dynamically to a Django formset). Needless to say, I never liked this solution. To make the entry quicker and reduce database hits, I decided to first build the array of key-values in javascript and submit with AJAX.

The simple template I used for a proof of concept was:

<div style="height: 500px" class="w3-container w3-padding-64 w3-theme-l5" id="contact">
    <div style="height: 50px"></div>
    <form method="post" class="w3-container w3-card-4 w3-display-top w3-padding-16 w3-white" id="form">
        {% csrf_token %}
        <div id="inputs">
            <div class="w3-row">
                <div class="w3-quarter">
                    <input type="text" name="key0" id="key0">
                </div>
                <div class="w3-quarter">
                    <input type="text" name="value0" id="value0">
                </div>
            </div>
        </div>
        <div class="w3-quarter">
            <input type="button" name="add" value="add" onclick="add_another()">
        </div>
        <div class="w3-quarter">
            <input type="button" name="submit_data" value="submit_data" onclick="submit_pairs()">
        </div>
    </form>
</div>

You can see initial key-value inputs (key0 and value0) as well as add and submit buttons. When the user hits add, a new key-value pair is added to the DOM and the values in the inputs are saved to the array kv.

Note that there are better ways to save the data and I'll discuss the one I plan to implement at the end.

Since I already have key0 and value0 in the DOM, I must first add the input values to the array with the current i value (0) with:

kv.push({key: document.getElementById(key).value, value: document.getElementById(value).value});

I then increment i to create the new inputs by inserting the elements with the styling I need:

i++;
key = 'key' + i;
value = 'value' + i;

var div = document.createElement("div");
div.innerHTML = '<div class="w3-row">\n' +
    '<div class="w3-quarter">\n' +
    '<input type="text" name="'+ key + '" id="' + key + '">\n' +
    '</div>\n' +
    '<div class="w3-quarter">\n' +
    '<input type="text" name="' + value + '" id="' + value + '">\n' +
    '</div>\n' +
    '</div>\n';
document.getElementById("inputs").appendChild(div);

The final result is:

var i = 0;
var kv = [];

function add_another() {
    console.log('add');
    var key = 'key' + i;
    var value = 'value' + i;
    kv.push({key: document.getElementById(key).value, value: document.getElementById(value).value});
    i++;
    key = 'key' + i;
    value = 'value' + i;

    var div = document.createElement("div");
    div.innerHTML = '<div class="w3-row">\n' +
        '<div class="w3-quarter">\n' +
        '<input type="text" name="'+ key + '" id="' + key + '">\n' +
        '</div>\n' +
        '<div class="w3-quarter">\n' +
        '<input type="text" name="' + value + '" id="' + value + '">\n' +
        '</div>\n' +
        '</div>\n';
    document.getElementById("inputs").appendChild(div);

    for (i = 0; i < kv.length; i++) {
        console.log("key:- " + kv[i].key + " value:- " + kv[i].value);
    }
}

Now the data needs to be submitted. This was my first time using AJAX, but is was much easier than I expected. To submit (using jQuery), I built the function like this:

// AJAX for posting
function submit_pairs() {
    console.log("submit is working!"); // sanity check
    $.ajax({
        url : "", // the endpoint
        type : "POST", // http method
        data : { 'pairs' : JSON.stringify(kv), 'csrfmiddlewaretoken': '{{csrf_token}}'}, // data sent with the post request

        // handle a successful response
        success : function(json) {
            console.log(json); // log the returned json to the console
            console.log("success"); // another sanity check
        },
    });
}

Here, you can see we are posting the data as JSON to the current URL. The csrf_token must be passed along to prevent Cross-Site Request Forgery.

Handling it in the back-end is much like a normal view:

def pair(request):
    if request.method == 'POST':
        pairs = request.POST.get('pairs')
        pairs = json.loads(pairs)

        for pair in pairs:
            pair_model, created = Pair.objects.get_or_create(key=pair['key'], value=pair['value'])
            print(created)
            return JsonResponse({'status':200`, 'created': created})

    else:
        return render(request, "contributor/pairs.html")

The only difference to a normal view is the pairs = json.loads(pairs) where the JSON data to converted to a python dictionary. From there the data can be used like any other view.

The only thing left to do is error handling, but I haven't implemented that yet. The error info can be passed to the front end with JsonResponse to display any problems to the user.

The one thing I will be changing in production is how the data is saved to the array in add_another(). Right now if the user changes the data after "add" is pressed, the data won't be saved, so it would be best to build the array on submit (in a similar manner).

Any questions or suggestions? Please comment!

April 12, 2019, 8:53 a.m.