Simple Books List Web App (Pure Javascript)

Welcome to a tutorial on how to create a simple books list web app with pure Javascript. It’s the good old and boring school assignment. Yep, there are a ton of such tutorials all over the Internet, so here’s one that is slightly different. An offline books list progressive web app that is installable. Read on!

 

 

TABLE OF CONTENTS

 

JAVASCRIPT BOOKS LIST

All right, let us now get into more details on how the books list web app works.

 

 

 

PART 1) THE HTML

1-book-list.html
<!-- (A) BOOK LIST -->
<div id="wrap">
  <div id="list"></div>
  <div id="dummy" onclick="bl.toggle(true)">ADD BOOK</div>
</div>
 
<!-- (B) BOOK FORM -->
<div id="form"><form onsubmit="return bl.save()">
  <div id="fClose" onclick="bl.toggle(false)">X</div>
  <input type="hidden" id="fID">
  <label>Title</label>
  <input type="text" id="fTitle" required>
  <label>Author</label>
  <input type="text" id="fAuthor" required>
  <label>ISBN (optional)</label>
  <input type="text" id="fISBN">
  <input type="submit" value="Save">
</form></div>

The HTML should be pretty self-explanatory, there are only 2 main sections:

  1. <div id="list"> A container for Javascript to generate the books list into.
  2. <div id="form"> A hidden HTML form to add/edit a book entry.

 

 

PART 2) THE JAVASCRIPT

2A) INIT APP

2-book-list.js
var bl = {
  // (A) INIT
  data : null, // books list
  hList : null, // html books list
  hForm : null, // html book form
  fID : null, fTitle : null, fAuthor : null, fISBN : null, // html form fields
  init : () => {
    // (A1) GET HTML ELEMENTS
    bl.hList = document.getElementById("list");
    bl.hForm = document.getElementById("form");
    bl.fID = document.getElementById("fID");
    bl.fTitle = document.getElementById("fTitle");
    bl.fAuthor = document.getElementById("fAuthor");
    bl.fISBN = document.getElementById("fISBN");

    // (A2) LOAD ENTRIES
    bl.data = localStorage.getItem("books");
    if (bl.data==null) { bl.data = []; }
    else { bl.data = JSON.parse(bl.data); }

    // (A3) DRAW ENTRIES
    bl.draw();
  },
  // ...
};
window.onload = bl.init;

On window load, bl.init() will run. Beginners may get confused, but keep calm and look carefully.

  • (A1) Get all the related HTML elements.
  • (A2) We load book entries from localStorage into bl.data – For those who don’t know, this is persistent storage. Data saved in localStorage will not be lost when the user navigates away from the site.
  • (A3) Draw the book entries.

Yep, it’s just slightly long-winded. Not difficult.

 

2B) TOGGLE HTML FORM

2-book-list.js
// (B) TOGGLE FORM
toggle : id => {
  if (id===false) {
    bl.fID.value = "";
    bl.fTitle.value = "";
    bl.fAuthor.value = "";
    bl.fISBN.value = "";
    bl.hForm.classList.remove("show");
  } else {
    if (Number.isInteger(id)) {
      bl.fID.value = id;
      bl.fTitle.value = bl.data[id].t;
      bl.fAuthor.value = bl.data[id].a;
      bl.fISBN.value = bl.data[id].i;
    }
    bl.hForm.classList.add("show");
  }
},

Remember the hidden add/edit book HTML form? This bl.toggle() function is used to work with it. Take note of the id though.

  • id === false Reset the HTML form and hide it.
  • id === true Show the HTML form.
  • Number.isInteger(id) This indicates “edit mode”. bl.data is an array of books, and this will populate the HTML form with the book selected in bl.data[id].

 

 

2C) DRAW BOOKS LIST HTML

2-book-list.js
// (C) DRAW BOOKS HTML
draw : () => {
  let row;
  bl.hList.innerHTML = "";
  bl.data.forEach((book, i) => {
    row = document.createElement("div");
    row.className = "row";
    row.innerHTML = `<div class="rDel" onclick="bl.del(${i})">X</div>
    <div class="rTxt">
      <div class="rTitle">${book.t}${(book.i?" ("+book.i+")":"")}</div>
      <div class="rAuthor">${book.a}</div>
    </div>
    <div class="rEdit" onclick="bl.toggle(${i})">&#9998;</div>`;
    bl.hList.appendChild(row);
  });
},

Loop through bl.data and draw the HTML in <div id="list">.

 

2D) SAVE & DELETE BOOK

2-book-list.js
// (D) SAVE BOOK
save : () => {
  // (D1) GET DATA
  let data = {
    t : bl.fTitle.value,
    a : bl.fAuthor.value,
    i : bl.fISBN.value
  };

  // (D2) UPDATE DATA ARRAY
  if (bl.fID.value=="") { bl.data.push(data); }
  else { bl.data[parseInt(bl.fID.value)] = data; }
  localStorage.setItem("books", JSON.stringify(bl.data));

  // (D3) UPDATE HTML INTERFACE
  bl.toggle(false);
  bl.draw();
  return false;
},

// (E) DELETE BOOK
del : id => { if (confirm("Delete book?")) {
  bl.data.splice(id, 1);
  localStorage.setItem("books", JSON.stringify(bl.data));
  bl.draw();
}}

Lastly, remember that book entries are saved into localStorage? These 2 functions do just that.

 

 

PART 3) PROGRESSIVE WEB APP

 

3A) HTML HEADERS

1-books-list.html
<!-- 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="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="Books List">
<meta name="msapplication-TileImage" content="icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
 
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="3a-manifest.json">
 
<!-- SERVICE WORKER -->
<script>
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("3b-worker.js");
}
</script>

At this stage, we already have a fully functioning app. This step is optional, but to turn this into an “installable offline web app”, we need to add a few things to the HTML <head>.

  • HTML meta tags to define the icons and stuff. Pain in the “S” where everyone uses different meta tags and icon sizes. I figured to just use a huge 512 X 512 icon and let the platforms resize.
  • Define a web manifest file.
  • Register a service worker.

 

3B) WEB MANIFEST

3a-manifest.json
{
  "short_name": "Books",
  "name": "Books List",
  "icons": [{
    "src": "favicon.png",
    "sizes": "64x64",
    "type": "image/png"
  }, {
    "src": "icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "1-books-list.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

The web manifest file is what it is… A file that contains information on the web app – The name, icons, themes, settings, and more.

 

 

3C) SERVICE WORKER

3b-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("JSBooks")
    .then(cache => cache.addAll([
      "favicon.png",
      "icon-512.png",
      "1-book-list.html",
      "2-book-list.js",
      "2-book-list.css"
    ]))
    .catch(err => console.error(err))
  );
});
 
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
 
// (B) CREATE/INSTALL CACHE
self.addEventListener("install", (evt) => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open(cName)
    .then((cache) => { return cache.addAll(cFiles); })
    .catch((err) => { console.error(err) })
  );
});

// (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))
));

Lastly, a service worker is simply “Javascript that runs in the background”. For this worker:

  • (A) We create a new browser cache and store all the books list project files.
  • (C) Listen to fetch requests and “hijack” them. If the requested file is found in the cache, use it. Fall back to load from the network if not.

In other words, turning this into an offline app by loading it off the browser cache.

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

EXAMPLE CODE DOWNLOAD

Click here for the source code on GitHub gist, just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

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

This example will work on most modern “Grade A” browsers. “Installable” will only work on certain browsers and platforms at the time of writing.

 

LINKS & REFERENCES

 

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!