Simple Javascript Expense List (Progressive Web App)

Welcome to a tutorial on how to create a simple expense list with pure Javascript. Yep, there are a ton of such “expense list” examples on the Internet. So here’s one that is slightly different – An offline-capable expense list 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 & DEMO

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

 

QUICK NOTES

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.

 

EXPENSE LIST DEMO

 

 

JAVASCRIPT EXPENSE LIST

All right, let us now get into more details on how the Javascript expense list works.

 

PART 1) THE HTML

1-track-expense.html
<!-- (A) ADD NEW RECORD -->
<form id="add_form" onsubmit="return track.add()">
  <input id="add_date" type="date" required/>
  <input id="add_name" type="text" placeholder="Item Name" required/>
  <input id="add_amt" type="number" min="1" step="0.01" placeholder="Amount" value="1.00" required/>
  <input type="submit" value="Add"/>
</form>
 
<!-- (B) RECORDS -->
<div id="rec_wrap"></div>

The HTML layout is very straightforward:

  1. A <form> to “add a new expense entry” – Date, item name, and amount.
  2. An empty <div> to show the expense entries.

 

PART 2) THE JAVASCRIPT

2A) PROPERTIES

2-track-expense.js
var track = {
  // (A) PROPERTIES
  data : {},    // expenses data
  hDate : null, // html add item date
  hName : null, // html add item name
  hAmt : null,  // html add item amount
  hWrap : null, // html records wrapper
};

The var track object will contain all the mechanics that drive the app. At the very top of the script, we begin by defining the properties.

  • track.data An object to hold the expense entries. Will go through more below in 2E.
  • track.hSOMETHING Reference to the HTML elements.

 

 

2B) SAVE EXPENSE ENTRIES

2-track-expense.js
// (B) SUPPORT FUNCTION - SAVE TO LOCAL STORAGE
save : () => {
  localStorage.setItem("expenses", JSON.stringify(track.data));
},

Next, we have track.save() that saves track.data into the localStorage. For those who are new, track.data will disappear once the user navigates away from the page. This is why we have to save it into the persistent localStorage whenever changes are made.

 

2C) INITIALIZE

2-track-expense.js
// (C) INIT
init : () => {
  // (C1) GET HTML ELEMENTS
  track.hDate = document.getElementById("add_date");
  track.hName = document.getElementById("add_name");
  track.hAmt = document.getElementById("add_amt");
  track.hWrap = document.getElementById("rec_wrap");
 
  // (C2) DEFAULT TODAY'S DATE
  track.hDate.valueAsDate = new Date();
 
  // (C3) LOAD RECORDS FROM LOCAL STORAGE
  let data = localStorage.getItem("expenses");
  if (data != null) { track.data = JSON.parse(data); }
 
  // (C4) DRAW RECORDS
  track.draw();
},
window.addEventListener("DOMContentLoaded", track.init);

track.init() will run on page load, and it does what its name implies – Initiate the app.

  • (C1) Get the related HTML elements.
  • (C2) Set the default date in the “add entry” HTML form to today.
  • (C3) Restore the entries from localStorage.
  • (C4) Draw the expense records.

 

2D) DRAW EXPENSE ENTRIES

2-track-expense.js
// (D) DRAW RECORDS
draw : () => {
  // (D1) EMPTY WRAPPER
  track.hWrap.innerHTML = "";
 
  // (D2) LOOP & DRAW RECORDS
  let row, total = 0;
  for (let date in track.data) { for (let i in track.data[date]) {
    let entry = track.data[date][i];
    total += parseFloat(entry[1]);
 
    row = document.createElement("div");
    row.className = "row";
    row.innerHTML = `
    <input type="button" value="X" onclick="track.del('${date}', ${i})">
    <div class="name">[${date}] ${entry[0]}</div>
    <div class="amt">$${entry[1]}</div>`;
    track.hWrap.appendChild(row);
  }}
 
  // (D3) TOTAL
  row = document.createElement("div");
  row.className = "row total";
  row.innerHTML = `<div class="name">Total Expenses</div><div class="amt">$${total.toFixed(2)}</div>`
  track.hWrap.appendChild(row);
},

Right, this seems confusing at first, but keep calm and look closely. We are just looping through track.data, creating the HTML, and inserting them into <div id="rec_wrap">.

 

 

2E) ADD EXPENSE ENTRY

2-track-expense.js
// (E) ADD ENTRY
add : () => {
  // (E1) PUSH NEW ENTRY
  if (track.data[track.hDate.value]==undefined) { track.data[track.hDate.value] = []; }
  track.data[track.hDate.value].push([track.hName.value, track.hAmt.value]);
 
  // (E2) SORT & SAVE TO LOCALSTORAGE
  // CREDITS : https://www.w3docs.com/snippets/javascript/how-to-sort-javascript-object-by-key.html
  track.data = Object.keys(track.data).sort().reduce((r,k) => (r[k] = track.data[k], r), {});
  track.save();
 
  // (E3) RESET FORM
  track.hName.value = "";
  track.hAmt.value = "1.00";
 
  // (E4) DRAW RECORDS
  track.draw();
  return false;
},

As you already know track.data holds the expense entries. Here’s the data structure:

track.data = {
  "YYYY-MM-DD" : [["NAME", "AMOUNT"], ["NAME", "AMOUNT"], ...],
  "YYYY-MM-DD" : [["NAME", "AMOUNT"], ["NAME", "AMOUNT"], ...],
  ...
};

That’s right, track.data is an object organized by DATE : ARRAY OF ITEMS. In track.add(), we are simply pushing more entries into this object, sorting it, then updating it into localStorage.

 

2F) REMOVE EXPENSE ENTRY

2-track-expense.js
// (F) REMOVE ENTRY
del : (date, i) => { if (confirm("Delete Entry?")) {
  track.data[date].splice(i, 1);
  if (track.data[date].length==0) { delete track.data[date]; }
  track.save();
  track.draw();
}}

How do we remove an entry from track.data? We simply splice the selected entry out, and update localStorage.

 

PART 3) PROGRESSIVE WEB APP

3A) HTML META

1-track-expense.html
<head>
  <!-- ANDROID + CHROME + APPLE + WINDOWS APP -->
  <link rel="icon" href="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="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 Expense">
  <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>
</head>

At this stage, we already have a fully functioning expense tracker; This step is completely optional. But to bring it one level up to an “installable progressive web app”, we only need a few more things.

  • Specifying the icons is kind of optional. It’s a pain in the “S” as different platforms/browsers use different icon sizes. I usually just define a huge 512X512 icon, and let the platforms resize it.
  • Define a web app manifest.
  • Install a service worker.

 

 

3B) WEB MANIFEST

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

The manifest file should be pretty self-explanatory – It contains data on your app name, icons, and general settings.

 

3C) SERVICE WORKER

3b-worker.js
// (A) FILES TO CACHE
const cName = "Expenses",
cFiles = [
  "favicon.png",
  "icon-512.png",
  "1-track-expense.html",
  "2-track-expense.css",
  "2-track-expense.js"
];
 
// (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); })
  );
});

For those who are new once again, a “service-worker” is simply Javascript that runs in the background.

  • (A & B) In this one, we save the entire project into the browser cache.
  • (C) “Hijack” the fetch requests. Load from the cache if the requested file is found, fallback to load from the network if not.

In other words, this worker will help to support “offline mode” – Run from the browser cache.

 

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.

 

NOTES ON PERFORMANCE

A quick note here that localStorage is not a database, it can only save flat strings. In this example, we are saving data by converting an object into a string with JSON.stringify(). To restore the data, we convert a string back to an object with JSON.parse().

This is by all means, not great for performance. If you want a “real Javascript database”, check out indexed databases. Links below.

 

 

COMPATIBILITY CHECKS

This example will work on most browsers. But offline support and “install to home screen” will only work on modern “Grade A” browsers.

 

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!

2 thoughts on “Simple Javascript Expense List (Progressive Web App)”

  1. hello, I’m a fan of your work, I already bought one of your books and I’m about to buy another two, but what I would like to know is the theme of your site, because I would like to create a similar one for natives of my language, I am php programmer and I will focus on this to help south america beginners, if you can answer me I appreciate it.

    1. Thanks for the support, Code Boxx is running on WP Astra. A modified “lite version” with jQuery and Block library stripped.

Leave a Comment

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