Home

Boilerplate: Toggle A Django Checkbox With A Generic JavaScript Function Without Refreshing The Page

Introduction

This post shows how to use some JavaScript on a page to watch for changes to checkboxes on the page and send a request to Django to update the value without having to refresh the page. It assumes you already have a Django site/project set up by following the steps in Boilerplate: Create A New Django Site/Project , Boilerplate: Add Basic Template And Static File Handling In Django , and Boilerplate: Add Basic User Authencation To A Django Site .

It's also important to note that this version of the code doesn't work without javascript. I've got plans to make a more accessible version in the future.

Boilerplate Steps

  1. Make sure you've started the Python venv and are in the directory with: manage.py

    TODO: Write up how to make sure you're in the proper vecv

  2. Create the app

    terminal commands
    python manage.py startapp todos
  3. Make the todos templates directory

    terminal commands
    mkdir -p todos/templates/todos
  4. Add the app to INSTALLED_APPS=[]

    site_config/settings.py - section update
    INSTALLED_APPS = [
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.messages",
        "django.contrib.staticfiles",
        "todos.apps.TodosConfig",
    ]
  5. Update site_config/urls.py for todos

    site_config/urls.py - full content replacement
    from django.contrib import admin
    from django.urls import path
    from django.urls import include
    
    urlpatterns = [
        path("accounts/", include("django.contrib.auth.urls")),
        path("admin/", admin.site.urls),
        path("todos/", include("todos.urls")),
    ]
  6. Make the todos urls file

    todos/urls.py - new file to create
    from django.urls import path
    
    from . import views
    
    app_name = "todos"
    urlpatterns = [
        path("", views.index, name="index"),
        path("<int:pk>/toggle/", views.toggle, name="toggle"),
    ]
  7. Create the Todo model

    todos/models.py - full content replacement
    from django.db import models
    
    class Todo(models.Model):
        todo_text = models.CharField(max_length=200)
        is_done = models.BooleanField(default=False)
    
        def __str__(self):
            return self.todo_text
  8. Add the todos to the admin interface

    todos/admin.py - full content replacement
    from django.contrib import admin
    
    from .models import Todo 
    
    admin.site.register(Todo)
  9. Update todos/views.py

    todos/views.py - full content replacement
    from django.shortcuts import render
    from django.http import JsonResponse
    
    from todos.models import Todo 
    
    def index(request):
        todos = Todo.objects.all()
        context = { "todos": todos }
        return render(request, "todos/index.html", context)
    
    def toggle(request, pk):
        # NOTE: This just tests if a user is authenticated, not
        # for a specific user. That's a future enhancement to make
        if request.user.is_authenticated:
            if request.method == "POST":
                if request.POST['checked']:
                    check_value = request.POST['checked'].lower()
                    try:
                        todo = Todo.objects.get(pk=pk)
                        if check_value == "true":
                            todo.is_done = True 
                            todo.save()
                            return JsonResponse({"message": f"Updated {pk}"})
                        elif check_value == "false":
                            todo.is_done = False 
                            todo.save()
                            return JsonResponse({"message": f"Updated {pk}"})
                        else:
                            return JsonResponse({"message": f"The 'checked' value must be either 'true' or 'false'"}, status=406)
                    except:
                        return JsonResponse({"message": f"Could not get {pk}"}, status=400)
                else:
                    return JsonResponse({"message": f"Missing 'checked'"}, status=400)
            else:
                return JsonResponse({"message": f"Must be POST"}, status=405)
        else:
            return JsonResponse({"message": f"Not authenticated"}, status=403)
  10. Make the todos/index.html page template

    todos/templates/todos/index.html - new file to create
    <!DOCTYPE html>
    <html>
    
    <head>
    <script>
    function addCheckboxToggles() {
      const checkboxes = document.querySelectorAll('[type=checkbox]')
      checkboxes.forEach((cb) => {
        cb.addEventListener("change", toggleCheckbox)
      })
    }
    
    async function toggleCheckbox(event) {
      const el = event.target
      const csrf_token = document.querySelector(
        '[name=csrfmiddlewaretoken]'
      ).value
      const fd = new FormData()
      fd.append("csrfmiddlewaretoken", csrf_token)
      fd.append("checked", el.checked)
      const request = new Request(el.dataset.href, {
        method: "POST",
        body: fd
      });
      const response = await fetch(request);
      const json = await response.json();
      if (!response.ok) {
        console.log(`Error: ${json.message}`)
      } 
      else {
        console.log(`Ok: ${json.message}`)
      }
    }
    
    document.addEventListener("DOMContentLoaded", (event) => {
      addCheckboxToggles()
    })
    </script>
    </head>
    
    <body>
        {% include 'registration/login_status.html' %}
        <h1>To Dos</h1>
        <form>
          {% csrf_token %}
        <ul>
        {% for todo in todos %}
          <li>
            <input 
              type="checkbox"
              data-href="{% url 'todos:toggle' todo.id %}"
              {% if todo.is_done %}
                checked="checked"
              {% endif %}
              {% if not user.is_authenticated %}
                disabled
              {% endif %}
            />
            {{ todo.todo_text }}
          </li>
        {% endfor %}
        </ul>
        </form>
        <div>
          <input type="checkbox" name="unused_checkbox" /> Django doesn't try to manage this checkbox since it doesn't have a data-href attribute
        </div>
    </body>
    
    </html>
  11. Run the todos migration

    terminal commands
    python manage.py makemigrations todos
    python manage.py migrate
  12. Start the server

    terminal commands
    python manage.py runserver
  13. Add todos

    Go to the "todos" section of the admin interface and add a few Todos to play with. Tic the checkbox for "is_done" on some of them and leave it off for others.

    (You'll need to login with your superuser admin username/password if you haven't already)

  14. Visit the todos index page and play around with the checkboxes.

~ fin ~

Endnotes