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!

ⓘ 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 Books List Useful Bits & Links
The End

 

DOWNLOAD & NOTES

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

 

QUICK NOTES

  • Download and unzip into your HTTP folder.
  • Captain Obvious to the rescue – Use http://, not file:// to access 1-js-budget.html.
  • “Installable” will only work with https:// domains, http://localhost is an exception for testing.
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.

 

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

 

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="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. A 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) FILES TO CACHE
const cName = "JSBooks",
cFiles = [
  "favicon.png",
  "icon-512.png",
  "1-books-list.html",
  "2-books-list.js",
  "2-books-list.css"
];

// (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, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", (evt) => {
  evt.respondWith(
    caches.match(evt.request)
    .then((res) => { return res || fetch(evt.request); })
  );
});

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

  • (A & B) 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.

 

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

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!

Leave a Comment

Your email address will not be published.