Javascript Notes Progressive Web App (Free Download)

“Javascript Notes App”, just do a quick search and they are all over the Internet. But no, this is not another one of those “keep notes in local storage for beginners”. I figured the world needs better examples of modern web apps, so here it is, a “Notes PWA” that runs even when offline – You read that right, an installable and offline web app. Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES & REQUIREMENTS

  • This is not a “newbie-friendly” open-source project plus example. It involves the use of service workers, indexed database, cache storage.
  • Too much of a hassle to create an “online example”, just download the source code or grab it from Github.
  • “Grade A” browser required, alongside an https:// server. http://localhost is fine for testing too.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

SCREENSHOT

 

LICENSE & DOWNLOAD

This project is released under the MIT License. You are free to use it for your own personal and commercial projects, modify it as you see fit. On the condition that there the software is provided “as-is”. There are no warranties provided and “no strings attached”. Code Boxx and the authors are not liable for any claims, damages, or liabilities.

Download | GitHub

 

 

HOW IT WORKS

Not going to explain everything line-by-line (will take forever), but here’s a quick walkthrough of the Javascript Notes PWA.

 

PART 1) SINGLE PAGE APP

js-notes.html
<!-- (A) NOTES LIST -->
<div id="pgA">
  <div id="nAdd" class="mi" onclick="notes.show()">
    add_circle_outline
  </div>
  <div id="nList"></div>
</div>
 
<!-- (B) NOTE FORM -->
<div id="pgB"><form onsubmit="return notes.save()">
  <label>Title</label>
  <input id="nTitle" type="text" autocomplete="off" required>
  <label>Text</label>
  <textarea id="nText" autocomplete="off" required></textarea>
  <label>Text Color</label>
  <input id="ntColor" type="color" value="#ffffff">
  <label>Background Color</label>
  <input id="nbColor" type="color" value="#000000">
  <div id="nAction">
    <input type="button" class="mi" value="reply" onclick="notes.toggle('B')">
    <input type="button" id="nDel" class="mi" value="delete" onclick="notes.del()">
    <input type="submit" class="mi" value="save">
  </div>
</form></div>

That’s right, there’s only one HTML file in the entire project.

  • <div id="pgA"> The “main screen” where we show all the notes.
  • <div id="pgB"> An add/edit note form.

 

 

PART 2) JAVASCRIPT – INDEXED DATABASE

assets/js-notes-db.js
// (A) INDEXED DB
const IDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
var notesDB = {
  // (B) INITIALIZE DATABASE
  db : null,
  init : () => new Promise((resolve, reject) => {
    // (B1) OPEN NOTES DATABASE
    notesDB.db = IDB.open("JSNotes", 1);
 
    // (B2) CREATE NOTES DATABASE
    notesDB.db.onupgradeneeded = e => {
      // (B2-1) NOTES DATABASE
      notesDB.db = e.target.result;
 
      // (B2-2) IDB UPGRADE ERROR
      notesDB.db.onerror = e => {
        alert("Indexed DB upgrade error - " + evt.message);
        console.error(e);
        reject(e.target.error);
      };
 
      // (B2-3) EVENTS STORE
      if (e.oldVersion < 1) {
        let store = notesDB.db.createObjectStore("notes", {
          keyPath: "id",
          autoIncrement: true
        });
      }
    };
 
    // (B3) IDB OPEN OK
    notesDB.db.onsuccess = e => {
      notesDB.db = e.target.result;
      resolve(true);
    };
 
    // (B4) IDB OPEN ERROR
    notesDB.db.onerror = e => {
      alert("Indexed DB init error - " + e.message);
      console.error(e)
      reject(e.target.error);
    };
  }),
 
  // (C) TRANSACTION "MULTI-TOOL"
  tx : (action, store, data, idx) => new Promise((resolve, reject) => {
    // (C1) GET OBJECT STORE
    let req, tx = notesDB.db.transaction(store, "readwrite").objectStore(store);
 
    // (C2) PROCESS ACTION
    switch (action) {
      // (C2-1) NADA
      default: reject("Invalid database action"); break;
 
      // (C2-2) ADD
      case "add":
        req = tx.add(data);
        req.onsuccess = e => resolve(true);
        break;
 
      // (C2-3) PUT
      case "put":
        req = tx.put(data);
        req.onsuccess = e => resolve(true);
        break;
 
      // (C2-4) DELETE
      case "del":
        req = tx.delete(data);
        req.onsuccess = e => resolve(true);
        break;
 
      // (C2-5) GET
      case "get":
        req = tx.get(data);
        req.onsuccess = e => resolve(e.target.result);
        break;
 
      // (C2-6) GET ALL
      case "getAll":
        req = tx.getAll(data);
        req.onsuccess = e => resolve(e.target.result);
        break;
 
      // (C2-7) CURSOR
      case "cursor":
        if (idx) { resolve(tx.index(idx).openCursor(data)); }
        else { resolve(tx.openCursor(data)); }
        break;
    }
    req.onerror = e => reject(e.target.error);
  })
};

An indexed database is a huge component of this project, and this library handles it.

  • notesDB.init() Runs on page load to setup and “install” the database. Basically just creates a JSNotes table with a single notes store (table).
  • notesDB.tx() Runs a database transaction – Add, edit, delete, get, etc…

 

 

PART 3) NOTES JAVASCRIPT

3A) INITIALIZE

assets/js-notes.js
var notes = {
  // (A) INIT APP
  init : async () => {
    // (A1) REQUIREMENTS CHECK - INDEXED DB
    if (!IDB) {
      alert("Your browser does not support indexed database.");
      return;
    }
 
    // (A2) REQUIREMENTS CHECK - STORAGE CACHE
    if (!"caches" in window) {
      alert("Your browser does not support cache storage.");
      return;
    }
 
    // (A3) REGISTER SERVICE WORKER
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("CB-worker.js");
    }
 
    // (A4) DATABASE + INTERFACE
    if (await notesDB.init()) {
      notes.toggle("A");
      notes.list();
    }
  },
 
  // (B) TOGGLE PAGE
  toggle : p => document.getElementById("pg"+p).classList.toggle("show"),
  // ...
}
window.addEventListener("DOMContentLoaded", notes.init);

On window load, notes.init() will run. This should be pretty self-explanatory even if you “eyeball glance” through it.

  • Check browser requirements.
  • Install a service worker. This is required to make this into an “installable web app”.
  • Initialize and set up the indexed database.

 

3B) LIST NOTES

assets/js-notes.js
// (C) LIST NOTES
list : async () => {
  // (C1) GET & "RESET" HTML LIST
  let nList = document.getElementById("nList");
  nList.innerHTML = "";
 
  // (C2) GET & DRAW ENTRIES
  for (let n of await notesDB.tx("getAll", "notes")) {
    let d = document.createElement("div");
    d.className = "note";
    d.style.color = n.tc;
    d.style.backgroundColor = n.bc;
    d.innerHTML = `<h1 class="title">${n.title}</h1>
    <div class="txt">${n.txt}</div>`;
    d.onclick = () => notes.show(n.id);
    nList.appendChild(d);
  }
},

notes.list() Get all the notes from the database, and draw the HTML list.

 

 

3C) SHOW NOTE

assets/js-notes.js
// (D) SHOW ADD/EDIT NOTE FORM
nid : null, // current note id
show : async (id) => {
  // (D1) EDIT NOTE
  if (id) {
    notes.nid = +id;
    let note = await notesDB.tx("get", "notes", notes.nid);
    document.querySelector("#nTitle").value = note.title;
    document.querySelector("#nText").value = note.txt;
    document.querySelector("#ntColor").value = note.tc;
    document.querySelector("#nbColor").value = note.bc;
    document.querySelector("#nDel").style.display = "block";
  }
 
  // (D2) ADD NOTE
  else {
    notes.nid = null;
    document.querySelector("#pgB form").reset();
    document.querySelector("#nDel").style.display = "none";
  }
 
  // (D3) OPEN NOTE FORM
  notes.toggle("B");
},

notes.show() Remember that there is a “notes form” in the HTML above? When the user clicks on “add note” or “edit note”, this function will set up and show the form accordingly.

 

3D) SAVE & DELETE NOTES

assets/js-notes.js
// (E) SAVE NOTE
save : () => {
  // (E1) DATA TO SAVE
  let data = {
    title : document.getElementById("nTitle").value,
    txt : document.getElementById("nText").value,
    tc : document.getElementById("ntColor").value,
    bc : document.getElementById("nbColor").value
  };
 
  // (E2) SAVE ENTRY
  if (notes.nid) {
    data.id = notes.nid;
    notesDB.tx("put", "notes", data);
  } else { notesDB.tx("add", "notes", data); }
 
  // (E3) DONE!
  notes.nid = null;
  notes.toggle("B");
  notes.list();
  return false;
},
 
// (F) DELETE NOTE
// id : delete this note id
del : async () => { if (confirm("Delete note?")) {
  await notesDB.tx("del", "notes", notes.nid);
  notes.nid = null;
  notes.toggle("B");
  notes.list();
}}
  • notes.save() Get notes data from the HTML form, and save it into the database.
  • notes.del() Remove a note from the database.

 

 

PART 4) INSTALLABLE WEB APP

4A) HTML META DATA

js-notes.html
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="CB-manifest.json">
 
<!-- ANDROID + CHROME + APPLE + WINDOWS APP -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="assets/icon-512.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Offline JS Notes">
<meta name="msapplication-TileImage" content="assets/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">

With the HTML and Javascript, this is already a perfectly working web app. But to turn this into an “installable web app”, we need to address 3 things:

  • Add the above HTML “web app metadata”.
  • Register a web manifest.
  • Register a service worker.

 

4B) WEB MANIFEST

CB-manifest.json
{
  "short_name": "Notes",
  "name": "JS Notes",
  "icons": [{
    "src": "assets/favicon.png",
    "sizes": "64x64",
    "type": "image/png"
  }, {
    "src": "assets/icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "js-notes.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

The manifest file is what it is – The app name, icon, theme, settings, etc…

 

4C) SERVICE WORKER

CB-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("NotesPWA")
    .then(cache => cache.addAll([
      "assets/favicon.png",
      "assets/head-notes-pwa.webp",
      "assets/icon-512.png",
      "assets/js-notes-db.js",
      "assets/js-notes.js",
      "assets/js-notes.css",
      "assets/maticon.woff2",
      "CB-manifest.json",
      "js-notes.html"
    ]))
    .catch(err => console.error(err))
  );
});
 
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
 
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", evt => evt.respondWith(
  caches.match(evt.request).then(res => res || fetch(evt.request))
));

If you have not heard of service workers, it is pretty much “Javascript that runs in the background”.

  • (A) Save all the project files into the browser cache.
  • (C) Hijack the fetch requests, serve the cached files if found, and fallback to the network if not.

Simply put, “installing” the app into the browser and enabling “offline mode”.

 

EXTRA BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

COMPATIBILITY CHECKS

Most of the required features are already well-supported on modern “Grade A” browsers.

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *