“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 cum 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.
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.
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
1A) THE HTML
<!-- (A) MAIN CONTENTS -->
<div id="cb-main"></div>
<!-- (B) INFO BOX -->
<div id="cb-info">
<div id="cb-info-ico" class="mi"></div>
<div id="cb-info-txt"></div>
</div>
The main page itself only has 2 components.
<div id="cb-main">
Where we AJAX load the pages into.<div id="cb-info">
A simple “popup notification” in the middle of the page. Dismisses itself in 2 seconds.
1B) THE JAVASCRIPT
var cb = {
// (A) REGISTER PAGES HERE
pages : {
home : {file:"home.inc", load:notes.list},
form : {file:"form.inc", load:notes.load}
},
// (B) LOAD PAGE
load : () => {
READ WINDOW.LOCATION.HASH, MAP TO CB.PAGES
THEN AJAX FETCH PAGE & PUT INTO #CB-MAIN
}
};
window.addEventListener("hashchange", cb.load);
The quick essentials of how single-page app is being handled here:
- We read the hash from
window.location.hash
. - “Translate and map” it to
cb.pages
. For example,#home
will map tocb.pages["home"]
. - Do an AJAX
fetch(cb.pages[HASH]["file"])
and put the contents into<div id="cb-main">
. - Lastly, run
cb.pages[HASH]["load"]
if defined.
Yes, I figured that some master expert code ninja trolls are going to say “this is so dumb, just use React/Angular/Vue/Whatever”.
P.S. There are not even 5 pages in this project. Does it make sense to add to the loading bloat?
PART 2) APP PAGES
2A) LIST PAGE
<!-- (A) HEADER -->
<header class="cb-head">
<!-- (A1) TITLE -->
<h1 class="cb-head-title">My Notes</h1>
<!-- (A2) BUTTONS -->
<div class="cb-head-btn">
<button class="btn-ico mi" onclick="notes.show()">add_box</button>
</div>
</header>
<!-- (B) NOTES LIST -->
<div id="notes-list" class="cb-body"></div>
<!-- (C) NOTE ROW TEMPLATE -->
<template id="note-template"><div class="note-row">
<div class="note-left">
<div class="note-title"></div>
<div class="note-text"></div>
<div class="note-time"></div>
</div>
<button class="note-del btn-ico-grey mi">delete</button>
<button class="note-edit btn-ico-grey mi">edit</button>
</div></template>
This is the main page and the list of notes itself. Shouldn’t be difficult to figure out.
<header class="cb-head">
The “blue” header bar.<div id="notes-list">
The list of notes.<template id="note-template">
HTML template for the notes.
2B) ADD/EDIT NOTE PAGE
<!-- (A) HEADER -->
<header class="cb-head">
<!-- (A1) TITLE -->
<h1 class="cb-head-title" id="note-form-title"></h1>
<!-- (A2) BUTTONS -->
<div class="cb-head-btn">
<a class="btn-ico mi" href="#home">reply</a>
<button class="btn-ico mi" id="note-form-del">delete</button>
</div>
</header>
<!-- (B) NOTES FORM -->
<form id="notes-form" class="cb-body cb-form" onsubmit="return notes.save()">
<label for="note-title" class="form-field">Title</label>
<input id="note-title" class="form-field" type="text" autocomplete="off" required/>
<label for="note-text" class="form-field">Text</label>
<textarea id="note-text" class="form-field" autocomplete="off" required></textarea>
<input class="form-field btn-red" id="note-save" type="submit" value="Save"/>
</form>
Another easy one. Just the header and an HTML form to add/edit the notes.
PART 3) APP INIT
// (A) HELPER FUNCTION TO GENERATE ERROR MESSAGE
err : (msg) => {
let row = document.createElement("div");
row.className = "error";
row.innerHTML = msg;
document.getElementById("cb-main").appendChild(row);
},
// (B) INIT APP
iDB : null, iTX : null, // idb object & transaction
ready : 0, // number of ready components
init : () => {
// (B1) ALL CHECKS & COMPONENTS GOOD TO GO?
if (ready==1) {
notes.ready++;
if (notes.ready==2) { cb.load(); }
}
// (B2) REQUIREMENT CHECKS & SETUP
else {
CHECK BROWSER REQUIREMENTS
INSTALL SERVICE WORKER
PREPARE INDEXED DATABASE
}
},
window.addEventListener("DOMContentLoaded", notes.init);
The app initialization is kind of confusing, but the quick basics:
- (A)
notes.err()
Nothing but a helper function to display “big red error messages” on the screen. - (B)
notes.init()
Runs on page load, B2 will start first, doing all the browser checks and setup. Then, B1 will only proceed to start the app when everything is in place.
PART 4) NOTES ENGINE
The rest of assets/js-notes.js
is the “notes engine” itself:
- (C)
notes.list()
Draw the notes in “nice HTML”. - (D)
notes.show()
Show the selected note in the add/edit form. - (E)
notes.load()
A follow up ofnotes.show()
, loads the given note if editing. - (F)
notes.save()
Add or update a note. - (G)
notes.del()
Self-explanatory. Delete a note.
PART 5) SERVICE WORKER
// (A) FILES TO CACHE
const cName = "MyNotes",
cFiles = [FILES TO CACHE];
// (B) CREATE/INSTALL CACHE
self.addEventListener("install", (evt) => {
evt.waitUntil(
caches.open(cName)
.then((cache) => { return cache.addAll(cFiles); })
.catch((err) => { console.error(err) })
);
});
// (C) LOAD FROM CACHE, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", (evt) => {
evt.respondWith(
caches.match(evt.request)
.then((res) => { return res || fetch(evt.request); })
);
});
The service worker is a simple one – It caches the entire app itself so that it can run even when offline.
PART 6) INSTALLABLE PWA
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="js-notes-manifest.json">
With the service worker and indexed database, the app itself should already work offline. This is just one last bit – Add a manifest to make it installable.
USEFUL 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
- Arrow Functions – CanIUse
- Service Workers – CanIUse
- Cache Storage – CanIUse
- Indexed DB – CanIUse
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!