Simple Budget Planner PWA (Pure Javascript)

Welcome to a tutorial on how to create a simple budget planner web app using Javascript. Yep, there are a ton of such “budget planner” app examples all over the Internet. So here’s one that is slightly different – A pure Javascript budget planner that is installation and offline-capable. Read on!

 

 

TABLE OF CONTENTS

 

JAVASCRIPT BUDGET PLANNER

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

 

 

 

PART 1) THE HTML

1-js-budget.html
<!-- (A) SUMMARY -->
<div id="summary">
  <!-- (A1) BALANCE -->
  <div id="balance">
    <div id="balanceTxt">Balance</div>
    <div id="balanceAmt">$0</div>
  </div>
 
  <!-- (A2) INCOME -->
  <div id="income">
    <div id="incomeTxt">Income</div>
    <div id="incomeAmt">$0</div>
  </div>
 
  <!-- (A3) EXPENSE -->
  <div id="expense">
    <div id="expenseTxt">Expense</div>
    <div id="expenseAmt">$0</div>
  </div>
</div>
 
<!-- (B) ENTRIES LIST -->
<div id="entries">
  <div id="dummy" onclick="bg.toggle(true)">New Entry</div>
  <div id="list"></div>
</div>
 
<!-- (C) ENTRY FORM -->
<div id="form"><form onsubmit="return bg.save()">
  <div id="formEnd" onclick="bg.toggle(false)">X</div>
  <input type="hidden" id="formID">
  <label>Income/Expense</label>
  <select id="formSign" required>
    <option value="+">Income</option>
    <option value="-">Expense</option>
  </select>
  <label>Description</label>
  <input type="text" id="formTxt" required>
  <label>Amount</label>
  <input type="number" id="formAmt" step="0.01" required>
  <input type="submit" value="Save" id="formGo">
</form></div>

The HTML page looks confusing at first, but there are essentially only 3 main sections.

  1. The summary is right at the top – Available balance, and total income/expenses.
  2. A container to hold the list of income/expense entries. Will use Javascript to generate these.
  3. Lastly, a hidden add/edit entry form.

 

 

PART 2) THE JAVASCRIPT

2A) INITIALIZE APP

2-js-budget.js
var bg = {
  // (A) INITIALIZE
  data : null, // entries data
  hBal : null, // html balance amount
  hInc : null, // html income amount
  hExp : null, // html expense amount
  hList : null, // html entries list
  hForm : null, // html form wrapper
  fID : null, fSign : null, fTxt : null, fAmt : null, // html form fields
  init : () => {
    // (A1) GET HTML ELEMENTS
    bg.hBal = document.getElementById("balanceAmt");
    bg.hInc = document.getElementById("incomeAmt");
    bg.hExp = document.getElementById("expenseAmt");
    bg.hList = document.getElementById("list");
    bg.hForm = document.getElementById("form");
    bg.fID = document.getElementById("formID");
    bg.fSign = document.getElementById("formSign");
    bg.fTxt = document.getElementById("formTxt");
    bg.fAmt = document.getElementById("formAmt");

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

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

On window load, bg.init() will run. Yep, it looks “massive”, but keep calm and look closely.

  • (A1) Get the related HTML elements.
  • (A2) Load the income/expense entries from localStorage into bg.entries. For those who are new, localStorage is simply persistent storage. Data will remain even when the user navigates away from the site.
  • (A3) Draw the HTML entries list.

 

2B) TOGGLE HTML FORM

2-js-budget.js
// (B) TOGGLE FORM
toggle : id => {
  if (id===false) {
    bg.fID.value = "";
    bg.fSign.value = "+";
    bg.fTxt.value = "";
    bg.fAmt.value = "";
    bg.hForm.classList.remove("show");
  } else {
    if (Number.isInteger(id)) {
      bg.fID.value = id;
      bg.fSign.value = bg.entries[id].s;
      bg.fTxt.value = bg.entries[id].t;
      bg.fAmt.value = bg.entries[id].a;
    }
    bg.hForm.classList.add("show");
  }
},

Next, we have a helper function to toggle show/hide the HTML form. Nothing much really, just take note of the id parameter.

  • id === false Reset the HTML form fields and hide the form.
  • id === true Show the HTML form.
  • Number.isInteger(id) Indicates “edit mode”. Set the form fields to the selected id in bg.entries.

 

 

2C) DRAW HTML ENTRIES

2-js-budget.js
// (C) DRAW ENTRIES
draw : () => {
  // (C1) BALANCE, INCOME, EXPENSES
  let bal = 0, inc = 0, exp = 0, row;

  // (C2) LOOP & DRAW HTML ENTRIES
  bg.hList.innerHTML = "";
  bg.entries.forEach((entry, i) => {
    if (entry.s=="+") {
      inc += entry.a;
      bal += entry.a;
    } else {
      exp += entry.a;
      bal -= entry.a;
    }
    row = document.createElement("div");
    row.className = `entry ${entry.s=="+"?"income":"expense"}`;
    row.innerHTML = `<div class="eDel" onclick="bg.del(${i})">X</div>
    <div class="eTxt">${entry.t}</div>
    <div class="eAmt">$${parseFloat(entry.a).toFixed(2)}</div>
    <div class="eEdit" onclick="bg.toggle(${i})">&#9998;</div>`;
    bg.hList.appendChild(row);
  });
 
  // (C3) UPDATE TOTALS
  bg.hBal.innerHTML = bal<0 ? `-$${Math.abs(bal).toFixed(2)}` : `$${bal.toFixed(2)}` ;
  bg.hInc.innerHTML = `$${inc.toFixed(2)}`;
  bg.hExp.innerHTML = `$${exp.toFixed(2)}`;
},

Well, we are pretty much just looping through bg.entries and generating the HTML here. Also, calculating the totals.

 

2D) SAVE & DELETE ENTRIES

2-js-budget.js
// (D) SAVE ENTRY
save : () => {
  // (D1) GET DATA
  let data = {
    s : bg.fSign.value,
    t : bg.fTxt.value,
    a : parseFloat(bg.fAmt.value)
  };

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

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

// (E) DELETE ENTRY
del : id => { if (confirm("Delete entry?")) {
  bg.entries.splice(id, 1);
  localStorage.setItem("entries", JSON.stringify(bg.entries));
  bg.draw();
}}

Remember that data is stored in the localStorage? These two functions are doing just that.

 

 

PART 3) PROGRESSIVE WEB APP

 

3A) HTML HEADERS

1-js-budget.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="Budget">
<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. But to level this up into an installable offline web app, we need to set 3 things:

  • Define a whole load of icons and app meta data in the HTML <head> section. Kind of a pain to deal with as every platform is different. I figure the lazy way is to just provide a huge 512 X 512 icon and let the platforms resize it.
  • Define a web manifest file.
  • Register a service worker.

 

3B) WEB MANIFEST

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

The web manifest file should be pretty self-explanatory. It contains the app name, icons, themes, and setting.

 

 

3C) SERVICE WORKER

3b-worker.js
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
  self.skipWaiting();
  evt.waitUntil(
    caches.open("JSBudget")
    .then(cache => cache.addAll([
      "favicon.png",
      "icon-512.png",
      "1-js-budget.html",
      "2-js-budget.js",
      "2-js-budget.css"
    ]))
    .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))
});

Lastly, a service worker is simply a piece of Javascript that runs in the background. In this one:

  • (A) We create a storage cache and save all the project files into the browser.
  • (C) Listen and “hijack” fetch requests. Load the requested file from the cache if it is found, and fall back to the network if not.

In other words, loading the entire app off the browser cache and supporting offline mode.

 

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

The “basic features” will work on most modern browsers. But “installable web app” will only work on a few browsers 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. Required fields are marked *