DEV Community

muncey
muncey

Posted on • Edited on

Creating an online budget tool 4/5

The next step in creating an online budget tool is to add the ability to save data between sessions. In this case I am using local storage in the browser. It is not the most secure solution but it will demonstrates the techniques that you need to use to create a form that will save your budget.

The key to being able to make is to create a global click handler for the budgetTable table which will map buttons to actions based on the className of each buttons.

document.getElementById('budgetTable').addEventListener('click', function($ev) {
  const idx = $ev.target.dataset.idx;
  if ($ev.target.className.indexOf('edit-button') > -1) {
    editBudgetItem(idx);
  } else if ($ev.target.className.indexOf('delete-button') > -1) {
    deleteItem(idx);
  } else if ($ev.target.className.indexOf('save-button') > -1) {
    save(idx);
  } else if ($ev.target.className.indexOf('cancel-button') > -1) {
    cancelEdit();
  }
});
Enter fullscreen mode Exit fullscreen mode

The end goal of this is a form that looks like this:
image
It is obviously not styled as yet but it demonstrates the ability to add items, edit and delete items. The data is held for now in localStorage and in future I will look at setting up a back end so that the data can be held securely in a database but for now localStorage will do.
image
The lower level code to save and load the budget makes use of the window.localStorage object to get and set items. Items in local storage are held using name/value pairs and you will typically use JSON.stringify to prepare items for saving and JSON.parse to read items back. The logic is that if there is no my-budget in local storage I will create a default budget with sample data.

let budgetItems = [{
  item: 'Car',
  amount: 1.00
}]

const loadBudget = (storageKey) => {
  const budget = window.localStorage.getItem(storageKey);
  if (budget) {
    budgetItems = JSON.parse(budget);  
  }
}

const saveBudget = (storageKey) => {
  const budget = JSON.stringify(budgetItems);
  window.localStorage.setItem(storageKey, budget);
}
Enter fullscreen mode Exit fullscreen mode

I have added two new functions renderActions and renderEditRow. The renderActions will render the edit and delete button and the renderEditRow will render a budget item row as a form with a save and cancel button. Note the use of a specific class on both which will be used in the table click handler.

const renderActions = (idx) => {
  return `
  <button type="button" class="edit-button" data-idx="${idx}">Edit</button>
  <button type="button" class="delete-button" data-idx="${idx}">Delete</button>`
}


const renderEditRow = (data, idx) => {

  return `<tr>
            <td><input type="text" id="editItem" value="${data.item}"></td>
            <td><input type="number" id="editAmount" value="${parseFloat(data.amount)}"></td>
            <td>
              <button type="button" class="save-button" data-idx="${idx}">Save</button>
              <button type="button" class="cancel-button" data-idx="${idx}">Cancel</button>
            </td>
          </tr>`
}
Enter fullscreen mode Exit fullscreen mode

I have made a small change to renderRow to add an additional column for actions (edit/delete). Because the renderRow is also used for the totals row I also configure the function to only renderActions when idx is not null.

const renderRow = (data, idx) => {
  return `<tr>
            <td>${data.item}</td>
            <td>$${data.amount}</td>
            <td>${idx != null ? renderActions(idx) : '' }</td>
          </tr>`
};
Enter fullscreen mode Exit fullscreen mode

The renderRows function becomes a bit more complicated:

const renderRows = (data, idx) => {
  const html = [];
  for (let i=0; i<data.length; i++) {
    if (idx != null && idx == i) {
      html.push(renderEditRow(data[i], i));
    } else if (idx != null && idx != i) {
      html.push(renderRow(data[i]));
    } else {
      html.push(renderRow(data[i], i));
    }
  }
  return html.join('');
}
Enter fullscreen mode Exit fullscreen mode

This change is to render an edit row if the user wants to edit a certain row.

Next I add some utility functions to edit, save, delete and cancel.

const addBudgetItem = () => {
  const budgetItem = {
    item: document.getElementById('newItem').value,
    amount: document.getElementById('newAmount').value
  }
  budgetItems.push(budgetItem);
  document.getElementById('newItem').value = null;
  document.getElementById('newAmount').value = null;
}

const editBudgetItem = (idx) => {
  id = 'budgetTable';

  document.getElementById('newItem').setAttribute('disabled', true);
  document.getElementById('newAmount').setAttribute('disabled', true);
  document.getElementById('addButton').setAttribute('disabled', true);

  document.getElementById(id).tBodies[0].innerHTML = renderRows(budgetItems, idx);
}

const cancelEdit = () => {
  id = 'budgetTable';

  document.getElementById('newItem').setAttribute('disabled', false);
  document.getElementById('newAmount').setAttribute('disabled', false);
  document.getElementById('addButton').setAttribute('disabled', false);

  document.getElementById(id).tBodies[0].innerHTML = renderRows(budgetItems);
}

const save = (idx) => {

  budgetItems[idx].item = document.getElementById('editItem').value;
  budgetItems[idx].amount = parseFloat(document.getElementById('editAmount').value);

  saveBudget('my-budget');
  renderPage('budgetTable');

  document.getElementById('newItem').setAttribute('disabled', false);
  document.getElementById('newAmount').setAttribute('disabled', false);
  document.getElementById('addButton').setAttribute('disabled', false);
}

const deleteItem = (idx) => {
  const temp = [];
  for (let i=0; i < budgetItems.length; i++) {
    if (i != idx) {
      temp.push(budgetItems[i]);
    }
  }
  budgetItems = temp;

  saveBudget('my-budget');
  renderPage('budgetTable');
}
Enter fullscreen mode Exit fullscreen mode

At the end of each function if I change data in budgetItems I call saveBudget followed by renderPage.

So this gives me a functional form which can be used for personal use. In my next article I am planning to discuss how to style the form so that it looks great and is ready for dropping into a CMS (WordPress, Wix, Joomla) of your choice.

I have saved changes into a local-storage branch.

https://github.com/muncey/MyBudgetFrontEnd/tree/local-storage

Top comments (1)

Collapse
 
dallasapper profile image
dallasapper • Edited

Wow. That looks absolutely great. In these types of situations, I regret I am not good at coding. It is unbelievable how much one can do if one is good at coding and has the necessary perseverance. I personally had to go for the ynab budget, which is great as well and works perfectly fine. Moreover, it helped a lot to structure our family's finances, but being able to come up with one yourself and personalize it, that's a whole new level. Did you think about doing a project out of it and monetizing it?