Table of Contents


Introduction


Single Page Applications (SPA) are all the rage these days. The ecosystem of Javascript frameworks is constantly evolving. Key players these days include the various versions of Angular, React, Vue, Ember, and, to some extent, knockout.js (KO), which is the subject of this tutorial. KO is not as popular as the first players I mentioned, but one thing that I like about it is that it plays well in cases where you want to take advantage of intermingling python + django + javascript to develop your app -as opposed to using python / django strictly for the backend and pure javascript (in the form of Angular, or React) for the front end. Note that the latter approach (signified by the bold text) is what the experts recommend. I tend to disagree in cases where a full blown SPA is not warranted, and you would do well taking advantage of the many batteries that come with a framework like django (example: authentication).

This tutorial will walk you through the steps that you would take in django to implement, from a django perspective, the tutorial that the official KO web site walks readers through to implement loading and saving data to and from the server, as well as binding the data to it to instantly refresh the data as the user interacts with the application.

Again, the intent of this tutorial is not to teach about KO (the KO official tutorials are quite good), but to deal with django/python to get the data that KO needs.

Conventions throughout this tutorial


Please review the conventions page, which will help you understand how I communicate with you through this blog.

Setting up our template


The first thing that we will do is setup a template to render our data.

I used the django boilerplate that I covered on the setting up a django project boilerplate tutorial. You can download the boilerplate from github. Instructions to get it setup on your system can be found there.

The following pieces of code will give us the template that we need to render a list of tasks:

apps/main/urls.py

...

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^knockout-tasks/$', views.knockout_tasks, name='knockout_tasks'), 
]

apps/main/views.py

def knockout_tasks(request):
    return render(request, 'main/knockout-tasks.html', context=None)

apps/templates/main/knockout-tasks.html

... 

{% block content %}

    <div class="jumbotron centered-text">
        <h2>knockout.js tasks</h2>
    </div>

    <div class="container">
        <h3>Tasks</h3>

        <form data-bind="submit: addTask">
            Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?"/>
            <button type="submit">Add</button>
        </form>

        <ul data-bind="foreach: tasks, visible: tasks().length > 0">
            <li>
                <input type="checkbox" data-bind="checked: isDone"/>
                <input data-bind="value: title, disable: isDone"/>
                <a href="#" data-bind="click: $parent.removeTask">Delete</a>
            </li>
        </ul>

        You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
        <span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>
    </div>


{% endblock %}

{% block scripts %}
{% endblock %}

The above code comes straight from the KO tutorial.

After running the server and pointing the browser to the right address, I get the following:

 tumbling programmer - django and knockout.js - template showing the task list

At the moment the Add button and the Delete link don't work for we haven't programmed them to do anything, yet.

Setting up knockout.js on our template


For this tutorial, we will use a CDN to load the library from. Let's add the following to our knockout-tasks.html file:

{% block head %}

    <script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js'></script>

{% endblock %}

Further documentation regarding the installation of KO can be found here.

Adding the javascript code


From the tutorial page, let's copy the javascript code and paste it into our template file (knockout-tasks.html), as follows:

...
{% block scripts %}
    <script>
        function Task(data) {
            this.title = ko.observable(data.title);
            this.isDone = ko.observable(data.isDone);
        }

        function TaskListViewModel() {
            // Data
            var self = this;
            self.tasks = ko.observableArray([]);
            self.newTaskText = ko.observable();
            self.incompleteTasks = ko.computed(function () {
                return ko.utils.arrayFilter(self.tasks(), function (task) {
                    return !task.isDone()
                });
            });

            // Operations
            self.addTask = function () {
                self.tasks.push(new Task({title: this.newTaskText()}));
                self.newTaskText("");
            };
            self.removeTask = function (task) {
                self.tasks.remove(task)
            };
        }

        ko.applyBindings(new TaskListViewModel());
    </script>
{% endblock %}

By now, with the few lines of code shown above, KO accomplishes a few magical things, including data binding and live updating of the page / html when we add or delete tasks (without refreshing the whole page).

below are a couple of screenshots that result from adding and deleting tasks.

 tumbling programmer - django and knockout.js - adding tasks

 tumbling programmer - django and knockout.js - deleting tasks

Beautiful!

KO is ready to receive data from our django server.

On step 2 of the KO tutorial, we can read:

On this server, there's some code that handles requests to the URL /tasks, and responds with JSON data. Add code to the end of TaskListViewModel to request that data and use it to populate the tasks array:

function TaskListViewModel() {
    // ... leave the existing code unchanged ...

    // Load initial state from server, convert it to Task instances, then populate self.tasks
    $.getJSON("/tasks", function(allData) {
        var mappedTasks = $.map(allData, function(item) { return new Task(item) });
        self.tasks(mappedTasks);
    });    
}

We don't want to add that to our code because we want to test if we can serve that data with our django server, which is the whole purpose of this tutorial.

Let's inspect what the data looks like, by pointing our browser to the following address: http://learn.knockoutjs.com/tasks/. I get the following:

[{"title":"Wire the money to Panama","isDone":true},{"title":"Get hair dye, beard trimmer, dark glasses and \"passport\"","isDone":false},{"title":"Book taxi to airport","isDone":false},{"title":"Arrange for someone to look after the cat","isDone":false}]

Or, if we beautify it:

[
    {
        "title": "Wire the money to Panama",
        "isDone": true
    },
    {
        "title": "Get hair dye, beard trimmer, dark glasses and \"passport\"",
        "isDone": false
    },
    {
        "title": "Book taxi to airport",
        "isDone": false
    },
    {
        "title": "Arrange for someone to look after the cat",
        "isDone": false
    }
]

Let's create our django model to provide the data for our template.

The django model to hold data for knockout.js


Let's add the following code to our apps/main/models.py file to create the model that we need for our KO app:

class Task(models.Model):
    title = models.CharField(max_length=60)
    isDone = models.BooleanField(default=False)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "Tasks"
        verbose_name_plural = "Tasks"
        ordering = ["title"]

Let's add the model to our app's admin module so we can edit our data. We do that by adding the following code to our apps/main/admin.py file:

...
from main.models import Task


class TaskAdmin(admin.ModelAdmin):
    list_display = ('title',
                    'isDone',
                    )
admin.site.register(Task, TaskAdmin)

Let's run ./manage.py makemigrations and ./manage.py migrate to migrate our model. If not done yet, make sure to also run ./manage.py createsuperuser so you can access the admin page of your django app.

We can now log on to our admin page to make sure that our Task model was successfully registered with it.

 tumbling programmer - django and knockout.js - the admin page

Let's go ahead and add a few tasks to it. Below is what I added:

 tumbling programmer - django and knockout.js - our tasks

Setting up our ajax view


Let's add a view to our apps/main/views.py file to handle the ajax requests from KO. Here is the full blown code with comments explaining the key lines of code:

from django.shortcuts import render, get_object_or_404
from .models import Task
from django.http import JsonResponse
import json

... 

def ajax_knockout_tasks(request):   
    data = [] # initialize the array for the data to be processed / sent to the client
    if request.method == 'POST': 
        tasks = json.loads(request.body.decode('utf-8')) # retrieve the tasks from the ajax request
        for task in tasks: 
            if 'pk' in task: # task exists
                model_task = get_object_or_404(Task, pk=task['pk'])
                if '_destroy' in task: # task is to be deleted
                    model_task.delete()
                else: # task is to be updated
                    model_task.title = task['title']
                    model_task.isDone = task['isDone']
                    model_task.save()
            else: # create new task
                model_task = Task(title=task['title'],isDone=task['isDone'])
                model_task.save()
        data = 'success' # let the client know that things went OK on the server side
        return JsonResponse(data, safe=False) # safe=False is required to avoid security exploits
    else:
        tasks = Task.objects.all()  # retrieve all tasks     
        for task in tasks:
            # build the JSON object (our tasks)
            # not the most efficient way of doing it (more on that later)
            data.append({"title":task.title,"isDone":task.isDone, "pk":task.pk}) 
        return JsonResponse(data, safe=False) # send tasks to the client

We need to let django know where the client can access the above view. We do that by adding the following line to our apps/main/urls.py file:

...
urlpatterns = [
    ...
    url(r'^ajax/knockout-tasks/$', views.ajax_knockout_tasks, name='ajax_knockout_tasks'),
]

At this point, we can test our view by pointing our browser to the following address (after firing up our django server, if it's not up and running): http://127.0.0.1:8000/ajax/knockout-tasks/

Here is what the result looks like on Firefox:

 tumbling programmer - django and knockout.js - testing the ajax view

Binding knockout.js to our django ajax view


Let's program our javascript / KO front end to perform CRUD (create, read, update, delete) operations on our django model via ajax requests.

Reading data


To get the data from our ajax view, let's add a $.getJSON function to the KO's model at the bottom of the apps/templates/main/knockout-tasks.html file, like so:

{% block scripts %}
    <script>
        ...

        function TaskListViewModel() {
            ...

            $.getJSON("/ajax/knockout-tasks/", function (allData) {
                var mappedTasks = $.map(allData, function (item) {
                    return new Task(item)
                });
                self.tasks(mappedTasks);
            });
        }
        ko.applyBindings(new TaskListViewModel());
    </script>
{% endblock %}

If we run the django server and visit http://127.0.0.1:8000/knockout-tasks/, our browser should render the following:

 tumbling programmer - django and knockout.js - retrieving tasks from the django view

Note that I improved the appearance of the page a little bit by adding inline styling to some of the html elements, as shown below.

 tumbling programmer - django and knockout.js - improving the appearance of our tasks

At this point in time, our binding works one-way only (getting data from django). We can test this by adding or deleting tasks and hitting refresh after that. Because the data is not being saved on our server model, the browser will render the same information again. Let's fix that.

Before doing it, though, let's follow django's best practices and replace the hard coded address with our named view, by replacing $.getJSON("/ajax/knockout-tasks/" with $.getJSON("{% url 'ajax_knockout_tasks' %}". After making the change, hit refresh on your browser to make sure that it works.

Creating, updating, and deleting data


Let's add the following pieces of code to the script at the bottom of our apps/templates/main/knockout-tasks.html file; the new lines are signified by comments, which explain the reason for adding them:

{% block scripts %}
    <script>
        function Task(data) {
            this.title = ko.observable(data.title);
            this.isDone = ko.observable(data.isDone);
            this.pk = ko.observable(data.pk); //we need this to tell django which instance of the model to update
        }
        function TaskListViewModel() {
            // Data
            var self = this;
            self.tasks = ko.observableArray([]);
            self.newTaskText = ko.observable();
            self.incompleteTasks = ko.computed(function () {
                return ko.utils.arrayFilter(self.tasks(), function (task) {
                    return !task.isDone()
                });
            });

            // Operations
            self.addTask = function () {
                self.tasks.push(new Task({title: this.newTaskText(), isDone: false}));
                self.newTaskText("");
            };
            self.removeTask = function (task) {
                // self.tasks.remove(task)      // the KO tutorial uses this line but we actually
                self.tasks.destroy(task)        // need KO to flag the task with the _destroy property
                                                // so we can tell django which tasks to delete
            };

            // ajax request to perform Read operation on our data
            $.getJSON("{% url 'ajax_knockout_tasks' %}", function (allData) {
                var mappedTasks = $.map(allData, function (item) {
                    return new Task(item)
                });
                self.tasks(mappedTasks);
            });

            // ajax request to perform CUD operations on our data
            self.save = function () {
                $.ajax("{% url 'ajax_knockout_tasks' %}", {
                    data: ko.toJSON(self.tasks),
                    type: "POST", contentType: "application/json",  // Always make sure to specify POST
                                                                    // to avoid security holes
                    success: function (result) {
                        alert(result)
                    }
                });
            };
        }
        ko.applyBindings(new TaskListViewModel());
    </script>
{% endblock %}

We also need to add a button to save the data, like so:

{% block content %}
          ...

        You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
        <span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>

        <div>
            <button class="btn btn-primary" data-bind="click: save">Save changes</button>
        </div>
    </div>
{% endblock %}

If we run our server, add / delete / update tasks, and hit save, nothing happens; we are supposed to get an alert with some sort of message. Upon inspecting our django server's log, we can see that django is complaining about the lack of a CSRF token:

Forbidden (CSRF token missing or incorrect.): /ajax/knockout-tasks/

More on that here. I'm glad django has my back :)

Let's fix that.

CSRF protection for ajax requests


The django documentation recommends to do the following:

First, let's use https://github.com/js-cookie/js-cookie:

A simple, lightweight JavaScript API for handling cookies

We hook it up to our app by downloading and placing a copy of the js.cookie.js file under our apps/main/static/js folder.

We then load it on our knockout-tasks.html file by adding the following code to it:

{% block scripts %}
    <script src="{% static 'js/js.cookie.js' %}"></script>
    <script>
    ...

We then set the token on the ajax request by adding the following code to the javascipt portion of our knockout-tasks.html file:

{% block scripts %}
    <script src="{% static 'js/js.cookie.js' %}"></script>
    <script>    
        var csrftoken = Cookies.get('csrftoken');
        function csrfSafeMethod(method) {
            // these HTTP methods do not require CSRF protection
            return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
        }
        $.ajaxSetup({
            beforeSend: function (xhr, settings) {
                if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                    xhr.setRequestHeader("X-CSRFToken", csrftoken);
                }
            }
        });
        function Task(data) {
        ...

Let's run our server, edit the data, and hit the save button to test our setup. We should get an alert telling us about our success, like so:

 tumbling programmer - django and knockout.js - successful crud operations

One thing that is not working is that the count of incomplete tasks is not calculating properly as we add or complete tasks. This happened because we are using the self.tasks.destroy(task) line on our KO TaskListViewModel() function, and we didn't tell the self.incompleteTasks function to account for that. We fix it by adding && !task._destroy to it, like so:

...
self.incompleteTasks = ko.computed(function () {
    return ko.utils.arrayFilter(self.tasks(), function (task) {
        return !task.isDone() && !task._destroy
    });
});
...

Once done, we can test our app, which now should give us a proper tally of incomplete tasks.

This completes our tutorial, which I hope have armed you with knowledge to get you started with coupling knockout.js plus django, which could be a powerful combination for developing desktop-like applications.

A couple of things to consider


I took a sure but rather primitive approach as far as getting the data from django into a JSON like format. I encourage the reader to look into django's serialization tools (including but not limited to the django REST framework), which coupled with KO's implementation of a mapping plugin should provide for a more efficient way of getting the data to and from django.

Something else worth checking out is the django-knockout project, which is supposed to ease the implementation of KO with django. That may be the subject of a future tutorial :)