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
<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
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
// (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":""}">✓</div>
<div class="txt">${data.t}</div>
<div class="del">✖</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
// (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
// (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 totrue/false
.td.del()
Delete the selected item.
PART 3) PROGRESSIVE WEB APP
3A) PAGE META HEADERS
<!-- (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
{
"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
// (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…