Javascript Events Calendar PWA (Free Code Download)

Javascript Calendar, there sure are a ton of these all over the Internet. So I figured to do something a little different. This is a small experiment of mine to create a Javascript Events Calendar progressive web app – That’s right, this is a serverless, installable web app that works even when the user is offline. 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 and example. It involves the use of service workers, cache storage, and an indexed database.
  • “Grade A” browser required, alongside a 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, and 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 Calendar PWA.

 

PART 1) SINGLE PAGE HTML

js-calendar.html
<!-- (A) PERIOD SELECTOR -->
<div id="calHead">
  <div id="calPeriod">
    <input id="calBack" type="button" class="mi" value="navigate_before">
    <select id="calMonth"></select>
    <input id="calYear" type="number">
    <input id="calNext" type="button" class="mi" value="navigate_next">
  </div>
  <input id="calAdd" type="button" class="mi" value="add">
</div>
 
<!-- (B) CALENDAR WRAPPER -->
<div id="calWrap">
  <div id="calDays"></div>
  <div id="calBody"></div>
</div>
 
<!-- (C) EVENT FORM -->
<dialog id="calForm"><form method="dialog">
  <div id="evtCX" class="mi">clear</div>
  <h2 class="evt100">CALENDAR EVENT</h2>
  <div class="evt50">
    <label>Start</label>
    <input id="evtStart" type="datetime-local" required>
  </div>
  <div class="evt50">
    <label>End</label>
    <input id="evtEnd" type="datetime-local" required>
  </div>
  <div class="evt50">
    <label>Text Color</label>
    <input id="evtColor" type="color" value="#000000" required>
  </div>
  <div class="evt50">
    <label>Background Color</label>
    <input id="evtBG" type="color" value="#ffdbdb" required>
  </div>
  <div class="evt100">
    <label>Event</label>
    <input id="evtTxt" type="text" required>
  </div>
  <div class="evt100">
    <input type="hidden" id="evtID">
    <input type="button" id="evtDel" class="mi" value="delete">
    <input type="submit" id="evtSave" class="mi" value="save">
  </div>
</form></dialog>

Look no further, this is the only HTML page in the entire project.

  1. <div id="calHead"> The header bar.
    • <div id="calPeriod"> Month and year selectors.
    • <input id="calAdd"> Add new event button.
  2. <div id="calWrap"> The calendar will be generated here with Javascript.
  3. <dialog id="calForm"> Popup calendar event form.

 

 

PART 2) CALENDAR JAVASCRIPT

2A) PROPERTIES

assets/js-calendar.js
var cal = {
  // (A) PROPERTIES
  // (A1) FLAGS & DATA
  mon : false, // monday first
  events : null, // events data for current month/year
  sMth : 0, // selected month
  sYear : 0, // selected year
  sDIM : 0, // number of days in selected month
  sF : 0, // first date of the selected month (yyyymmddhhmm)
  sL : 0, // last date of the selected month (yyyymmddhhmm)
  sFD : 0, // first day of the selected month (mon-sun)
  sLD : 0, // last day of the selected month (mon-sun)
  ready : 0, // to track loading
 
  // (A2) HTML ELEMENTS
  hMth : null, hYear : null, // month & year
  hCD : null, hCB : null, // calendar days & body
  hFormWrap : null, hForm : null, // event form
  hfID : null, hfStart : null, // event form fields
  hfEnd : null, hfTxt : null,
  hfColor : null, hfBG : null,
  hfDel : null,
 
  // (A3) INDEXED DB
  iName : "JSCalendar", 
  iDB : null, iTX : null, // idb object & transaction
 
  // (A4) HELPER FUNCTIONS
  toDate : date => parseInt(date.replace(/-|T|:/g, "")),
  toISODate : date => {
    date = String(date);
    yr = date.slice(0,4); mth = date.slice(4,6); day = date.slice(6,8);
    hr = date.slice(8,10); min = date.slice(10);
    return `${yr}-${mth}-${day}T${hr}:${min}`;
  },
  //...
};

All the calendar mechanics are contained within var cal = {}. There are a lot of settings, flags, and data… Not going to go through all of them one by one, just take note of mon : false – Change this if you want the week to start on Monday.

 

2B) INITIALIZE

assets/js-calendar.js
// (B) INIT
init : () => {
  // (B1) REQUIREMENTS CHECK - INDEXED DB
  window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  if (!window.indexedDB) {
    alert("Your browser does not support indexed database.");
    return;
  }

  // (B2) REQUIREMENTS CHECK - STORAGE CACHE
  if (!"caches" in window) {
    alert("Your browser does not support cache storage.");
    return;
  }

  // (B3) OPEN IDB
  let req = window.indexedDB.open(cal.iName, 1);

  // (B4) IDB OPEN ERROR
  req.onerror = evt => {
    alert("Indexed DB init error - " + evt.message);
    console.error(evt);
  };

  // (B5) IDB UPGRADE NEEDED
  req.onupgradeneeded = evt => {
    cal.iDB = evt.target.result;

    // (B5-1) IDB UPGRADE ERROR
    cal.iDB.onerror = evt => {
      alert("Indexed DB upgrade error - " + evt.message);
      console.error(evt);
    };

    // (B5-2) IDB VERSION 1
    if (evt.oldVersion < 1) {
      let store = cal.iDB.createObjectStore(cal.iName, {
        keyPath: "id",
        autoIncrement: true
      });
      store.createIndex("s", "s", { unique: false });
      store.createIndex("e", "e", { unique: false });
    }
  };
 
  // (B6) IDB OPEN OK
  req.onsuccess = evt => {
    cal.iDB = evt.target.result;
    cal.iTX = () => {
      return cal.iDB
      .transaction(cal.iName, "readwrite")
      .objectStore(cal.iName);
    };
    cal.prepare();
  };
},
window.onload = cal.init;

On page load, cal.init() will run. Looks complicated, but all this does is:

  • Checks if the browser supports indexedDB and caches.
  • Initialize and create the calendar indexedDB.

 

 

2C) INITIALIZE CALENDAR HTML

assets/js-calendar.js
// (C) PREPARE CALENDAR HTML INTERFACE
prepare : () => {
  // (C1) GET HTML ELEMENTS
  cal.hMth = document.getElementById("calMonth");
  cal.hYear = document.getElementById("calYear");
  cal.hCD = document.getElementById("calDays");
  cal.hCB = document.getElementById("calBody");
  cal.hFormWrap = document.getElementById("calForm");
  cal.hForm = cal.hFormWrap.querySelector("form");
  cal.hfID = document.getElementById("evtID");
  cal.hfStart = document.getElementById("evtStart");
  cal.hfEnd = document.getElementById("evtEnd");
  cal.hfTxt = document.getElementById("evtTxt");
  cal.hfColor = document.getElementById("evtColor");
  cal.hfBG = document.getElementById("evtBG");
  cal.hfDel = document.getElementById("evtDel");

  // (C2) MONTH & YEAR SELECTOR
  let now = new Date(), nowMth = now.getMonth() + 1;
  for (let [i,n] of Object.entries({
    1 : "January", 2 : "Febuary", 3 : "March", 4 : "April",
    5 : "May", 6 : "June", 7 : "July", 8 : "August",
    9 : "September", 10 : "October", 11 : "November", 12 : "December"
  })) {
    let opt = document.createElement("option");
    opt.value = i;
    opt.innerHTML = n;
    if (i==nowMth) { opt.selected = true; }
    cal.hMth.appendChild(opt);
  }
  cal.hYear.value = parseInt(now.getFullYear());

  // (C3) ATTACH CONTROLS
  cal.hMth.onchange = cal.load;
  cal.hYear.onchange = cal.load;
  document.getElementById("calBack").onclick = () => cal.pshift();
  document.getElementById("calNext").onclick = () => cal.pshift(1);
  document.getElementById("calAdd").onclick = () => cal.show();
  cal.hForm.onsubmit = () => cal.save();
  document.getElementById("evtCX").onclick = () => cal.hFormWrap.close();
  cal.hfDel.onclick = cal.del;

  // (C4) DRAW DAY NAMES
  let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  if (cal.mon) { days.push("Sun"); } else { days.unshift("Sun"); }
  for (let d of days) {
    let cell = document.createElement("div");
    cell.className = "calCell";
    cell.innerHTML = d;
    cal.hCD.appendChild(cell);
  }

  // (C5) LOAD & DRAW CALENDAR
  cal.load();
},

This is “part 2” of the initialization process. Once again, it looks massive. But all it does is pretty much:

  • Get the HTML elements.
  • Draw the month/year selectors.
  • Draw the day names (Monday to Sunday).
  • “Enable” the HTML interface.

 

2D) SHIFTING THE CURRENT PERIOD

assets/js-calendar.js
// (D) SHIFT CURRENT PERIOD BY 1 MONTH
pshift : forward => {
  cal.sMth = parseInt(cal.hMth.value);
  cal.sYear = parseInt(cal.hYear.value);
  if (forward) { cal.sMth++; } else { cal.sMth--; }
  if (cal.sMth > 12) { cal.sMth = 1; cal.sYear++; }
  if (cal.sMth < 1) { cal.sMth = 12; cal.sYear--; }
  cal.hMth.value = cal.sMth;
  cal.hYear.value = cal.sYear;
  cal.load();
},

This is just a small “convenience function” to help shift the current period by one month.

 

2E) LOADING CALENDAR EVENTS

assets/js-calendar.js
// (E) LOAD EVENTS DATA FOR MONTH/YEAR
load : () => {
  // (E1) SET SELECTED PERIOD
  cal.sMth = parseInt(cal.hMth.value);
  cal.sYear = parseInt(cal.hYear.value);
  cal.sDIM = new Date(cal.sYear, cal.sMth, 0).getDate();
  cal.sFD = new Date(cal.sYear, cal.sMth-1, 1).getDay();
  cal.sLD = new Date(cal.sYear, cal.sMth-1, cal.sDIM).getDay();
  let m = cal.sMth;
  if (m < 10) { m = "0" + m; }
  cal.sF = parseInt(String(cal.sYear) + String(m) + "010000");
  cal.sL = parseInt(String(cal.sYear) + String(m) + String(cal.sDIM) + "2359");
  
  // (E2) FETCH INIT
  // inefficient. but no other ways to do complex search in idb.
  cal.ready = 0;
  cal.events = {};
  let rangeA = IDBKeyRange.bound(cal.sF, cal.sL),
      rangeB = IDBKeyRange.lowerBound(cal.sL, true);
 
  // (E3) GET ALL START DATE THAT FALLS INSIDE MONTH/YEAR
  cal.iTX().index("s").openCursor(rangeA).onsuccess = evt => {
    let cursor = evt.target.result;
    if (cursor) {
      if (!cal.events[cursor.value.id]) {
        cal.events[cursor.value.id] = cursor.value;
      }
      cursor.continue();
    } else { cal.loading(); }
  };

  // (E4) GET ALL END DATE THAT FALLS INSIDE MONTH/YEAR
  cal.iTX().index("e").openCursor(rangeA).onsuccess = evt => {
    let cursor = evt.target.result;
    if (cursor) {
      if (!cal.events[cursor.value.id]) {
        cal.events[cursor.value.id] = cursor.value;
      }
      cursor.continue();
    } else { cal.loading(); }
  };

  // (E5) END DATE AFTER SELECTED MONTH/YEAR, BUT START IS BEFORE
  cal.iTX().index("e").openCursor(rangeB).onsuccess = evt => {
    let cursor = evt.target.result;
    if (cursor) {
      if (cursor.value.start<cal.sFirst && !cal.events[cursor.value.id]) {
        cal.events[cursor.value.id] = cursor.value;
      }
      cursor.continue();
    } else { cal.loading(); }
  };
},
 
// (F) LOADING CHECK
loading : () => {
  cal.ready++;
  if (cal.ready==3) { cal.draw(); }
},

cal.load() handles the loading of data from the indexedDB… But as you can see, this is not SQL.

  • There is literally no way to do a SELECT * FROM `EVENTS` WHERE (`START_DATE` BETWEEN X AND Y) OR (`END_DATE` BETWEEN X AND Y) OR (`START_DATE`<X AND `END_DATE`>Y).
  • The only way is to run 3 different “queries” in cal.load() to fetch the different time frames and combine the results.
  • cal.loading() will make sure that all the entries are ready before drawing the HTML calendar.

 

 

2F) DRAW CALENDAR HTML

assets/js-calendar.js
// (G) DRAW CALENDAR
draw : () => {
  // (G1) CALCULATE DAY MONTH YEAR
  // note - jan is 0 & dec is 11 in js
  // note - sun is 0 & sat is 6 in js
  let now = new Date(), // current date
      nowMth = now.getMonth()+1, // current month
      nowYear = parseInt(now.getFullYear()), // current year
      nowDay = cal.sMth==nowMth && cal.sYear==nowYear ? now.getDate() : null ;

  // (G2) DRAW CALENDAR ROWS & CELLS
  // (G2-1) INIT + HELPER FUNCTIONS
  let rowA, rowB, rowC, rowMap = {}, rowNum = 1,
      cell, cellNum = 1,
  rower = () => {
    rowA = document.createElement("div");
    rowB = document.createElement("div");
    rowC = document.createElement("div");
    rowA.className = "calRow";
    rowA.id = "calRow" + rowNum;
    rowB.className = "calRowHead";
    rowC.className = "calRowBack";
    cal.hCB.appendChild(rowA);
    rowA.appendChild(rowB);
    rowA.appendChild(rowC);
  },
  celler = day => {
    cell = document.createElement("div");
    cell.className = "calCell";
    if (day) { cell.innerHTML = day; }
    rowB.appendChild(cell);
    cell = document.createElement("div");
    cell.className = "calCell";
    if (day===undefined) { cell.classList.add("calBlank"); }
    if (day!==undefined && day==nowDay) { cell.classList.add("calToday"); }
    rowC.appendChild(cell);
  };
  cal.hCB.innerHTML = ""; rower();

  // (G2-2) BLANK CELLS BEFORE START OF MONTH
  if (cal.mon && cal.sFD != 1) {
    let blanks = cal.sFD==0 ? 7 : cal.sFD ;
    for (let i=1; i<blanks; i++) { celler(); cellNum++; }
  }
  if (!cal.mon && cal.sFD != 0) {
    for (let i=0; i<cal.sFD; i++) { celler(); cellNum++; }
  }

  // (G2-3) DAYS OF THE MONTH
  for (let i=1; i<=cal.sDIM; i++) {
    rowMap[i] = { r : rowNum, c : cellNum };
    celler(i);
    if (cellNum%7==0 && i!=cal.sDIM) { rowNum++; rower(); }
    cellNum++;
  }

  // (G2-4) BLANK CELLS AFTER END OF MONTH
  if (cal.mon && cal.sLD != 0) {
    let blanks = cal.sLD==6 ? 1 : 7-cal.sLD;
    for (let i=0; i<blanks; i++) { celler(); cellNum++; }
  }
  if (!cal.mon && cal.sLD != 6) {
    let blanks = cal.sLD==0 ? 6 : 6-cal.sLD;
    for (let i=0; i<blanks; i++) { celler(); cellNum++; }
  }
 
  // (G3) DRAW EVENTS
  if (Object.keys(cal.events).length > 0) { for (let [id,evt] of Object.entries(cal.events)) {
    // (G3-1) EVENT START & END DAY
    let sd = new Date(cal.toISODate(evt.s)),
        ed = new Date(cal.toISODate(evt.e));
    if (sd.getFullYear() < cal.sYear) { sd = 1; }
    else { sd = sd.getMonth()+1 < cal.sMth ? 1 : sd.getDate(); }
    if (ed.getFullYear() > cal.sYear) { ed = cal.sDIM; }
    else { ed = ed.getMonth()+1 > cal.sMth ? cal.sDIM : ed.getDate(); }

    // (G3-2) "MAP" ONTO HTML CALENDAR
    cell = {}; rowNum = 0;
    for (let i=sd; i<=ed; i++) {
      if (rowNum!=rowMap[i]["r"]) {
        cell[rowMap[i]["r"]] = { s:rowMap[i]["c"], e:0 };
        rowNum = rowMap[i]["r"];
      }
      if (cell[rowNum]) { cell[rowNum]["e"] = rowMap[i]["c"]; }
    }
 
    // (G3-3) DRAW HTML EVENT ROW
    for (let [r,c] of Object.entries(cell)) {
      let o = c.s - 1 - ((r-1) * 7), // event cell offset
          w = c.e - c.s + 1; // event cell width
      rowA = document.getElementById("calRow"+r);
      rowB = document.createElement("div");
      rowB.className = "calRowEvt";
      rowB.innerHTML = cal.events[id]["t"];
      rowB.style.color = cal.events[id]["c"];
      rowB.style.backgroundColor = cal.events[id]["b"];
      rowB.classList.add("w"+w);
      if (o!=0) { rowB.classList.add("o"+o); }
      rowB.onclick = () => cal.show(id);
      rowA.appendChild(rowB);
    }
  }}
},

cal.draw() Generates the HTML calendar. Quite a bit of calculation… Run through these in your own free time if you want.

 

2G) SHOW EVENT FORM

assets/js-calendar.js
// (H) SHOW EVENT FORM
show : id => {
  if (id) {
    cal.hfID.value = id;
    cal.hfStart.value = cal.toISODate(cal.events[id]["s"]);
    cal.hfEnd.value = cal.toISODate(cal.events[id]["e"]);
    cal.hfTxt.value = cal.events[id]["t"];
    cal.hfColor.value = cal.events[id]["c"];
    cal.hfBG.value = cal.events[id]["b"];
    cal.hfDel.style.display = "inline-block";
  } else {
    cal.hForm.reset();
    cal.hfID.value = "";
    cal.hfDel.style.display = "none";
  }
  cal.hFormWrap.show();
},

cal.show() To display the hidden popup event form.

  • id===NUMBER When the user clicks on an event to edit it. Load the event from indexDB and populate the form fields.
  • id===ANYTHING When the user clicks on “add”. Just show an empty event form.

 

 

2H) SAVE EVENT

assets/js-calendar.js
// (I) SAVE EVENT
save : () => {
  // (I1) COLLECT DATA
  // s & e : start & end date
  // c & b : text & background color
  // t : event text
  var data = {
    s : cal.toDate(cal.hfStart.value),
    e : cal.toDate(cal.hfEnd.value),
    t : cal.hfTxt.value,
    c : cal.hfColor.value,
    b : cal.hfBG.value
  };
  if (cal.hfID.value != "") { data.id = parseInt(cal.hfID.value); }
  console.log(data);

  // (I2) DATE CHECK
  if (new Date(data.s) > new Date(data.e)) {
    alert("Start date cannot be later than end date!");
    return false;
  }

  // (I3) SAVE
  if (data.id) { cal.iTX().put(data); }
  else { cal.iTX().add(data); }
  cal.hFormWrap.close();
  cal.load();
  return false;
},

Pretty self-explanatory. cal.save() gets the data from the HTML form and save the event into the indexedDB.

 

2I) DELETE EVENT

assets/js-calendar.js
// (J) DELETE EVENT
del : () => { if (confirm("Delete Event?")) {
  cal.iTX().delete(parseInt(cal.hfID.value));
  cal.hFormWrap.close();
  cal.load();
}}

Lastly, cal.del() deletes an event from the indexedDB.

 

PART 3) PROGRESSIVE WEB APP

3A) HTML META HEADERS

js-calendar.html
<!-- WEB APP & ICONS -->
<link rel="icon" href="assets/favicon.png" type="image/png">
<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="JS Calendar">
<meta name="msapplication-TileImage" content="assets/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
 
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="CB-manifest.json">
 
<!-- SERVICE WORKER -->
<script>if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("CB-worker.js");
}</script>

The calendar is pretty much already complete. But to create a PWA, there are 3 more things to address:

  • Add the above HTML meta headers to specify the icons/app name.
  • Register a web manifest.
  • Register a service worker.

 

3B) WEB MANIFEST

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

The manifest file is what it is… A file to indicate the app icons, name, settings, and more.

 

3C) SERVICE WORKER

CB-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("JSCalendar")
    .then(cache => cache.addAll([
      "assets/favicon.png",
      "assets/icon-512.png",
      "assets/HEAD-js-cal.jpg",
      "assets/maticon.woff2",
      "CB-manifest.json",
      "assets/js-calendar.css",
      "assets/js-calendar.js",
      "js-calendar.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))
));

For those who are new, a service worker is just a piece of Javascript that runs in the background. For this worker:

  • (A) Create a new storage cache and save the project files.
  • (C) “Hijack” the fetch requests. If the requested file is in the cache, use it. If not, load from the network.

In short, this worker will enable the web app to work in “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!

6 thoughts on “Javascript Events Calendar PWA (Free Code Download)”

  1. I would like to pass a department ID into the calendar class to view events by department, but not having much luck. I have a department drop down which passes a link to the calendar through the url ( ?dept=23 ). I would like to grab that and use it in the SQL in the calendar class but I’m struggling getting that value into the class. Any help you could give me would be greatly appreciated

    1. 1) Create multiple object stores for different departments.
      2) On changing dept dropdown – Fetch data from the selected object store and redraw the HTML. The end.

      Throw the ideas of “server-side” GET, POST, SQL database out of the window. They don’t exist here, this is a serverless offline app.

  2. i am learning about pwa and tried uploading this to a website to use it online and it works on chrome, but it does not seem to work on edge or safari. would you know why?

  3. This mini project is valuable for studies, and I would like a light on how I could make the offline push notification starting from the calendar event, will it be possible?

Leave a Comment

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