To Do List In HTML Javascript (Serverless Web App)

Once upon a time, a student laughed at a “to do list” assignment. She confidently said “it’s just local storage”, examples are all over the Internet. Those examples are cool, but they don’t demonstrate the capabilities of modern web apps. So here it is, a to do list that uses indexed database, is installable, and fully offline capable.

 

CODE DOWNLOAD

I have released this under the MIT license, feel free to use it in your own project – Personal or commercial. Some form of credits will be nice though. 🙂

 

 

VIDEO TUTORIAL

 

TO DO LIST DEMO

  • Double click on item text to edit.
  • Hit enter or click outside to save.
  • Hit escape to cancel.

 

 

PART 1) THE HTML

todo.html
<div id="tdWrap">
  <!-- (PART A) ADD TO DO ITEM -->
  <form id="tdAdd" onsubmit="return td.add()">
    <input id="tdAddTxt" type="text" placeholder="New Item" required>
    <input id="tdAddBtn" type="submit" value="Add">
  </form>

  <!-- (PART B) TO DO LIST ITEMS -->
  <div id="tdList"></div>
</div>

There are only 2 sections to the HTML.

  • <form id="tdAdd"> Add a new to do item.
  • <div id="tdList"> List of to do items.

 

 

PART 2) THE JAVASCRIPT

2A) INIT

todo.js
var td = {
  // (PART A) PROPERTIES
  hAdd : null, // html add item text
  hList : null, // html to do list
  db : null, // indexed database
  temp : null, // temporary value for edit

  // (PART B) HELPER - DB TRANSACTION
  tx : () => td.db.transaction("todo", "readwrite").objectStore("todo"),

  // (PART C) INITIALIZE
  init : () => {
    // (C1) GET HTML ELEMENTS
    td.hAdd = document.getElementById("tdAddTxt");
    td.hList = document.getElementById("tdList");

    // (C2) CREATE INDEXED DB
    td.db = window.indexedDB.open("todo", 1); 
    td.db.onupgradeneeded = e => {
      td.db = e.target.result;
      td.db.createObjectStore("todo", {
        keyPath: "id", autoIncrement: true
      });
    };

    // (C3) DB INIT OK - DRAW LIST
    td.db.onsuccess = e => {
      td.db = e.target.result;
      td.tx().getAll().onsuccess = e => {
        for (let row of e.target.result) { td.draw(row); }
      };
    };
  },
  ...
};
 
// (PART J) START
window.addEventListener("load", td.init);

The Javascript is pretty massive, let us walk through section by section.

  • All the mechanics are contained within the var td = {} object.
  • (J & C) On window load, we call td.init().
  • (C) The initialization is straighforward.
    • (C1) Get the HTML “add item form” and “to do list”.
    • (C2) Create a to do list indexed database.
    • (C3) Lastly, get all entries from the database and draw the to do list.
  • (B) A helper function to start a transaction with database – For get/add/put/delete.

 

 

2B) ADD & DRAW ITEMS

todo.js
// (PART D) ADD TO DO ITEM
add : () => {
  let data = {
    s : false,
    t : td.hAdd.value.trim()
  };
  td.tx().add(data).onsuccess = e => {
    data.id = e.target.result;
    td.draw(data);
    td.hAdd.value = "";
  };
  return false;
},

// (PART E) DRAW TO DO LIST
draw : data => {
  // (E1) CREATE ITEM ROW
  let row = document.createElement("div");
  row.className = "row";
  row.dataset.id = data.id;
  row.innerHTML = `<div class="stat${data.s?" done":""}">&#10003</div>
  <div class="txt">${data.t}</div>
  <div class="del">&#10006</div>`;

  // (E2) CLICK ACTIONS
  row.querySelector(".stat").onclick = () => td.toggle(row);
  row.querySelector(".txt").ondblclick = () => td.edit(row);
  row.querySelector(".del").onclick = () => td.del(row);
  td.hList.appendChild(row);
},
  • (D) td.add() Called when the “add item” form is submitted. Creates a new entry in the database, in the format of :
    • s : true/false Item is done/not done.
    • t : text The item text itself.
    • id : integer “Primary key”, automatically assigned by the database.
  • (E) td.draw() This is will draw a given item into the HTML list.

 

 

2C) EDIT ITEM

todo.js
// (PART F) EDIT ROW
edit : row => {
  // (F1) GET SEGMENTS & SAVE CURRENT ITEM
  let stat = row.querySelector(".stat"),
  txt = row.querySelector(".txt"),
  del = row.querySelector(".del");
  td.temp = txt.textContent;

  // (F2) DISABLE BUTTONS
  stat.onclick = "";
  txt.ondblclick = "";
  del.onclick = "";

  // (F3) ON EDIT END
  let end = commit => {
    // (F3-1) UPDATE HTML INTERFACE
    txt.onblur = "";
    txt.onkeyup = "";
    txt.contentEditable = false;
    stat.onclick = () => td.toggle(row);
    txt.ondblclick = () => td.edit(row);
    del.onclick = () => td.del(row);

    // (F3-2) COMMIT OR CANCEL
    if (commit) {
      let val = txt.textContent.trim();
      if (val=="" || val==td.temp) { txt.innerHTML = td.temp; }
      else { txt.innerHTML = val; td.put(row); }
    } else { txt.innerHTML = td.temp; }
  };

  // (F4) SET EDITABLE
  txt.contentEditable = true;
  txt.focus();
  txt.onblur = () => end(true);
  txt.onkeyup = e => {
    if (e.key == "Enter") { end(true); }
    if (e.key == "Escape") { end(false); }
  };
},

// (PART G) SAVE EDIT ROW
put : row => td.tx().put({
  s : row.querySelector(".stat").classList.contains("done"),
  t : row.querySelector(".txt").textContent,
  id : +row.dataset.id
}),
  • (F) td.edit() On double clicking an item, a couple of things will happen.
    • (F1 & F2) Disable “toggle status” and “delete item” while editing the item.
    • (F4) Set the text to editable, focus on it. Hit enter or click outside to commit, hit escape to cancel.
    • (F3) Restore “toggle status ” and “delete item” on edit end. Save/discard the changes accordingly.
  • (G) td.put() When the user commits the change, this function will update the database.

 

 

2D) TOGGLE STATUS & DELETE ITEM

todo.js
// (PART H) TOGGLE DONE
toggle : row => {
  row.querySelector(".stat").classList.toggle("done");
  td.put(row);
},

// (PART I) DELETE ITEM
del : row => { if (confirm("Delete item?")) {
  td.tx().delete(+row.dataset.id);
  row.remove();
}}
  • td.toggle() Update done/not done, pretty much toggle the status to true/false.
  • td.del() Delete the selected item.

 

PART 3) PROGRESSIVE WEB APP

 

3A) PAGE META HEADERS

todo.html
<!-- (PART C) PROGRESSIVE WEB APP -->
<link rel="icon" href="logo.png" type="image/png">
<link rel="manifest" href="manifest.json">
<script>if ("serviceWorker" in navigator) {
  // (C1) REGISTER SERVICE WORKER
  navigator.serviceWorker.register("worker.js", {scope: "/"});

  // (C2) CACHE WEB APP FILES
  caches.open("todo").then(cache => cache.addAll([
    "todo.css", "todo.html", "todo.js",
    "logo.png", "manifest.json"
  ]));
}</script>

With all the above, we already have a “functioning to do list”. To turn this into a “web app”, we need to add a couple of things. First, insert a couple of headers into the page header:

  • Specify a web app manifest file.
  • Register a service worker.
  • Create a storage cache and save all the project files inside.

 

 

3B) MANIFEST FILE

manifest.json
{
  "short_name": "To Do List",
  "name": "To Do",
  "icons": [{
    "src": "logo.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "/todo.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

What is a manifest file? As you can see, it contains information on the web app itself – Name, icons, start URL, colors, settings, etc…

 

3C) SERVICE WORKER

worker.js
// (PART A) LOAD FILE FROM CACHE, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", e => e.respondWith(
  caches.match(e.request).then(r => r || fetch(e.request))
));

Lastly, a “service worker” is a script that runs in the background. In this one, we listen to the fetch requests.

  • If the requested file is found in the storage cache – Use it.
  • Else, fallback and load the requested file from the server.
  • In short, turn this into an offline app that runs on the user’s device.

 

 

THE END – A COUPLE OF NOTES

That’s all for this tutorial and sharing. Before we end, just a short note that I have omitted a couple of things to keep this one “relatively simple”. There are quite a number of improvements that you can do:

  • Compatibility checks – Browser needs to support indexed database.
  • Some error handling – What happens when the database get/add/put/delete fails?
  • A “sort items” feature.
  • Capture more data – Category, color, date, time, etc…