Loan calculator project
Loan calculator project
Loan calculator with file I/O
We can make use of the File System API, assuming that we will use either the Chrome or Chromium browsers. So, the idea for this project is to make a loan calculator that can open/save JSON files with the basic loan information. This would allow the user to save their calculations and then bring up the saved calculations by opening their saved file. As an added convenience, we can store the loan data in localStorage so that if the user is on the same computer and browser, the previous calculations are already there as soon as they open the page.
Since we will be using localStorage, you may want to look back at this lesson: using_localStorage.
Creating the loan calculator page
To start this app, let’s start with a page that has the markup for a HTML table where we will store the loan information. Since we will build the data rows of this table programmatically, we will use a tbody element with an id. This will allow manipulating the rows with JavaScript. Here is the initial version of "loan_calculator.html":
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
function init() {
console.log('init called');
}
</script>
</head>
<body>
<h1>Loan data</h1>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
Lines 15-25 define a HTML table. Lines 16-23 form the <thead> section. This just creates the headings for the 4 columns of loan data that we will use. Line 24 defines the <tbody> element with an id="loan_tbody" attribute. This is the element that we need to get a reference to, so that we can use JavaScript to control the rows that comprise the data rows.
Line 5 makes it so that the init() function is called when the web page markup is finished loading. Line 6 defines the variable, loan_data, as an array.
Right now, all the init() function does, is print 'init called' to the console. Here is a screen shot of this page in the Chromium browser:
Adding a loan
To add a loan, there are several steps involved:
-
You need to put an add button into the markup, that has an appropriate id.
-
You need to create a dialog box that is displayed when you click on the add button. That dialog box should have some kind of Cancel button and an Ok button.
-
You need a function that is called when you click on the Ok button of the dialog, that will gather the user input from the dialog and update the loan_data array.
-
After the loan_data array is updated, you need to call another function that updates the table displaying the loan data.
Creating a Loan class
It will be useful to create a Loan class. The constructor of this class will read in the annual interest rate, the number of years for the loan and the amount borrowed as instance variables and then use those to calculate the monthly payment. The loan_data array, will be an array that holds Loan objects. This is what the Loan class will look like:
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to % and monthly interest rate
const n = this.years*12; // 12 periods per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
Let’s start by modifying the app so that the Loan class is defined and we construct a Loan object inside of the init() function to test our calculation of the monthly payment. Here is the next version of the code.
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function init() {
console.log('init called');
let loan1 = new Loan(5.5, 5, 35000);
console.log(loan1);
}
</script>
</head>
<body>
<h1>Loan data</h1>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
Lines 8-20 define the Loan class. Line 23 constructs a new Loan object and line 24 displays that object to the console.
Here is a screen shot showing the app running in the browser:
Checking the calculation in a spreadsheet, I could see that the monthly payment calculation was made correctly.
Putting in an add button and add dialog
Next, we can put an add button into the markup and then create a function called showAddDialog() that will display a modal dialog that allows the user to enter the loan parameters. Here is the new version of the app that will do this:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
Line 43 creates the add_button for adding a loan. Lines 44-54 create a <dialog> element. Line 45 is a prompt for the user to enter the annual interest rate, and line 46 creates the text input box for the user to enter that rate. Line 47 is a prompt for the user to enter the number of years for the load, and line 47 creates the text input box for the user to enter that number of years. Line 49 is a prompt for the user to enter the amount borrowed, and line 50 creates the text input box to collect that information. Line 52 adds a Cancel button and line 53 adds an Ok button.
Line 7 creates the variable add_dlg that will hold a reference to the add dialog box. Lines 23-25 define the showAddDialog() function. It is very simple, as all it does it use the reference to the add dialog box and call the showModal() function on that box. Lines 27-29 define the closeAddDialog() button. This is also very simple as it just calls close() using the reference to the add dialog box.
Line 35 is where the reference to the add dialog is obtained. Line 36 gets a reference to the 'add_cancel' button, so that we can associate the closeAddDialog() function with clicking on that button.
If you run the app in the browser, you can click on the Add a loan button and the add dialog box will pop up as shown in the next screen shot:
If you click on the Cancel button of the add dialog box, this will close that dialog box.
The Ok button still needs to be set up. We will need a function called handleAddOk to read in the values from the add dialog box and construct a Loan object. That function will then push this Loan object on to the loan_data array. Here is the next version of the app:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
console.log(loan_data);
add_dlg.close();
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are lines 31-42 and lines 51-52. Line 51 gets a reference to the add_ok button and line 52 makes it so that the handleAddOk() function is called when that button is clicked. Lines 31-42 define the handleAddOk() function. Lines 32-34 obtain references to the input text boxes in the add dialog. Lines 35-37 get the values from those input text boxes and convert them into numbers using the Number() function. Line 38 constructs a Loan object from the user input values, and line 39 adds that loan to the end of the loan_data array. Line 40 displays that array to the console. We will eventually use that data to populate our HTML table, but using the console will at least let us see if this is working correctly so far. Line 41 closes the add dialog box.
Here is a screen shot showing the app after two Loan objects have been added:
Updating the loan table
Next, we will create a function called updateTable() that will be used to update the HTML table for the loans. Here is the new version of the app that does this:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
//console.log(loan_data);
updateTable();
add_dlg.close();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
for (let loan of loan_data) {
let tr = document.createElement('tr');
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are lines 40-41 and lines 45-68. Line 40 comments out the line that displays the loan_data array to the console. Line 41 calls the updateTable() function that is defined on lines 45-68.
The updateTable() function defined on lines 45-68 looks like a lot of code, but it is mainly creating the HTML markup for table rows (<tr>) programmatically. Line 47 gets a reference to the loan_tbody element in the table. Lines 48-67 define a for loop that iterates over each element in the loan_data array. Line 49 creates a <tr> element. Line 50 creates a <td> element. Line 51 gets the loan’s rate and creates a TextNode from that value. Creating a TextNode allows appending that value to the <td> element, and that is done on line 52. Line 53 appends the <td> element to the <tr> element. Lines 54-65 repeat those steps to add the other <td> elements into the <tr> element. Finally, line 66 appends the <tr> element to the <tbody> element of the table.
The following screen shot shows what the app looks like after adding two loans. Note that there is an error that we need to fix:
As you can see, the first loan shows up twice. This is because the <tbody> element is being appended to, and so the previous loans are already in the <tbody> when we add subsequent loans. This can be fixed if we can clear the <tbody> each time we update the table. The next version of the app fixes this:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
//console.log(loan_data);
updateTable();
add_dlg.close();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let loan of loan_data) {
let tr = document.createElement('tr');
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
Lines 9-13 define the removeChildren() function. This function uses a while loop to continue to remove the first child of an element as long as children remain. Line 54 calls this function to remove the children from the <tbody> before the for loop adds in all the <tr> elements.
Here is a screen shot of the app showing that the repeated loan problem is fixed:
Using localStorage to store the loan data
Now that we can add loans, let’s make it so that the loan data is stored in localStorage. Here is the link to the lesson on using localStorage in case you need a refresher on this.
We need to take care of several things to use localStorage:
-
We need to use JSON.stringify() to convert the loan_data array into a string. Then, we can store it in localStorage.
-
We need to have a way to check to see if localStorage has the correct key for the data. If the correct key exists, then the value from localStorage must be converted into a JavaScript object using JSON.parse().
-
Once we have restored loan_data from localStorage, we need to call updateTable() to update the HTML table.
Here is the new version of the app that puts the above items into effect:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let loan of loan_data) {
let tr = document.createElement('tr');
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are lines 46-47 and lines 87 -90. Line 46 uses JSON.stringify() to convert loan_data into a string. Line 47 then stores that string in localStorage under they key loan_data. Line 87 checks to see if the key loan_data exists in localStorage. If that key exists, line 88 gets the value of that key, and uses JSON.parse() to restore the loan_data array. Line 89, then calls updateTable() to update the loan table.
The next screen shot shows that the loan data is stored in localStorage.
To see what is stored in localStorage, you can look at the Application tab in Dev Tools. On the left, select Local storage, then select the URL for your application. Then, you can lookup the key and that key’s value.
Editing loan parameters
An important feature to have for the app is to be able to edit any of the loan parameters. To do this, we need to have a way of letting the user select a row (loan) to edit. Then, a dialog box could be displayed that would show the current values and allow those values to be changed. This involves several steps:
-
The table rows can be made selectable by making it so that when the user clicks on a row, this causes an edit dialog box with the current values to be displayed. This will require adding some kind of id or index for each row, as well as adding an Event Listener so that a function is called whenever a row is clicked.
-
The updateTable() function must be updated to use an index or id. This could start with using an index-based for loop, instead of the "for each" style we are currently using.
-
The updateTable() function can also add an Event Listener to each row so that a function that displays the current loan parameters in a dialog box can be displayed. That dialog box would allow editing of the loan parameters.
Making table rows selectable using mouse clicks
Let’s make it so that a table row can be selected by the user clicking on any part of that row. To be able to do this, I will change the for loop in updateTable() to be an index-based for loop. This will allow me to use the index as an id for the loan data. In addition, I will make it so that clicking on a row will call a function called showEditDialog() that will display an edit dialog for the data in that row. Here is the new version of the app.
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
console.log(loan_data[index]);
}
function handleEditOk() {
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 8, 53-57, 59-61, 67-68, 70-71,101-105 and 127-137. Lines 127-137 are lines added to the markup for the edit dialog box. It is similar in layout to the add dialog box.
Line 101 gets the reference to the edit dialog box. Line 102 gets a reference to the Cancel button in the edit dialog and line 103 makes that button close the edit dialog box. Line 104 gets a reference to the Ok button in the edit dialog, and line 105 makes it so that the handleEditOk() function will be called when that Ok button is clicked.
Line 67 make the for loop that iterates over all the loan_data items an index-based for loop. Line 68 defines the variable loan so that much of the rest of the for loop can remain unchanged. Lines 70 and 71 are the only changes with the for loop. Line 70 gives the <tr> element an id attribute with a value equal to the for loop index. This will allow selecting the correct element from the loan_data array. Line 71 attaches an Event Listener so that clicking on the row will call the showEditDialog() function.
Lines 59-61 form the skeleton of the handleEditOk() function. This will need to be filled in later, but is only there so that when line 105 is executed there won’t be any error generated.
Lines 53-57 define the early version of the showEditDialog() function. Eventually, this function will obtain the loan parameters for the loan that was clicked on and populate the edit dialog box with those values. Then, the edit dialog box will be displayed (as a modal dialog). But, for now, all this function does is display event.currentTarget (line 54) and obtain the index for that table row on line 55. On line 56, that index is used to display the corresponding element from the loan_data array.
Line 8 just defines the variable edit_dlg that will be used to hold the reference to the edit dialog box.
Here is a screen shot showing the console output when the user clicks on the first loan in the table.
As you can see, the first line of output in the console shows event.currentTarget, the <tr> element. The second line of output shows loan_data[0].
Displaying the edit dialog box
Now that we can select a table row, let’s display the actual edit dialog box. Here is the new version of the app.
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 9, 57, 59-66 and 70-81. Line 9 defines the loan_id variable which will hold the index of the loan that was selected for editing. Line 57 stores the value for loan_id from the event.currentTarget’s id. Lines 59-66 are used to set up the edit dialog box and display that dialog box. Lines 59-61 obtain references to the input text boxes for the loan parameters. Line 62 gets the correct loan from the loan_data array. Lines 63-65 populates the input text boxes with the current loan parameters. LIne 66 causes the edit dialog box to be displayed as a modal dialog box.
Lines 70-81 complete the handleEditOk() function. Lines 70-72 get references for the input text boxes and lines 73-75 get the values and convert them into numbers using the Number() function. Line 76 constructs a new Loan object using those numbers. Line 77 replaces the current loan in the loan_id position of the array with the new Loan, l1. Line 78 uses JSON.stringify the loan_data array, and line 79 updates the value of the loan_data key inside localStorage. Line 80 calls updateTable() and line 81 closes the edit dialog box.
The next screen shot shows the app with three loans that have been edited to show the effect of increasing the annual interest rate by 0.25 percent.
Deleting a loan
Let’s modify the app so that we can delete a loan. Since we can select a row for editing, we can modify the edit dialog box to include an extra button for deleting that loan. Here are the steps that are involved:
-
Modify the markup for the <dialog id="edit_dlg"> element by adding a third button for Delete
-
Get a reference to that Delete button and associate a function that shows a delete dialog box. That function can be an anonymous function that just uses the showModal() function.
-
Add markup for a delete dialog box. This box will show a prompt "Are you sure?" and have two buttons. The first button will be a Cancel button that cancels the deletion. The second button will be a Yes button that will lead to the loan being deleted.
-
To delete a loan, the array splice() method will be used. After splice() is used, localStorage must be updated and updateTable() should be called.
Here is the new version of the app that implements deleting a loan:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let delete_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function handleDelete() {
const id = loan_id; // already stored by clicking on a table row
loan_data.splice(id, 1); // remove 1 element at position id
const loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
delete_dlg.close();
edit_dlg.close();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
delete_dlg = document.getElementById('delete_dlg');
const edit_delete = document.getElementById('edit_delete');
edit_delete.addEventListener('click', () => { delete_dlg.showModal(); });
const delete_cancel = document.getElementById('delete_cancel');
delete_cancel.addEventListener('click', () => { delete_dlg.close(); });
const delete_yes = document.getElementById('delete_yes');
delete_yes.addEventListener('click', handleDelete);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button><br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
<button id="edit_delete">Delete Loan</button>
</dialog>
<dialog id="delete_dlg">
Are you sure you want to delete the loan?<br>
<br>
<button id="delete_cancel">Cancel</button>
<button id="delete_yes">Yes</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 9, 85-93, 138-144, 176 and 178-183. Line 176 adds in the Delete button to the edit dialog. Lines 178-183 adds in the lines for the delete confirmation dialog.
Lines 138-144 add to the init() function by adding in the event listeners needed to support deleting a loan. Line 138 gets a reference to the delete confirmation dialog. Line 139 gets a reference to the Delete button in the edit dialog box, and line 140 associates clicking on that button with displaying the delete confirmation dialog. Line 141 gets a reference to the Cancel button in the delete confirmation dialog, and line 142 associates clicking on that button with closing the delete confirmation dialog. Line 143 gets a reference to the Yes button in the delete confirmation dialog, and line 144 associates clicking on that button with the handleDelete() function.
Lines 85-93 define the handleDelete() function. Line 86 retrieves the loan_id that was stored when we first clicked on a table row. Line 87 removes the loan in question using the splice() function. Line 88 stringifies loan.data and line 89 stores that string in the localStorage key named loan_data. Line 90 calls updateTable(), line 91 closes the delete confirmation dialog and line 92 closes the edit dialog (since handling delete handles that edit too).
Allow Saving as a JSON file
Now that we can add, edit and/or delete a loan, let’s use the File System API to save the loan data to a JSON file. Here is a new version of the app that saves the loan data to an output JSON file:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let delete_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function handleDelete() {
const id = loan_id; // already stored by clicking on a table row
loan_data.splice(id, 1); // remove 1 element at position id
const loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
delete_dlg.close();
edit_dlg.close();
}
async function handleSave() {
try {
const handle = await window.showSaveFilePicker();
const outfile = await handle.createWritable();
const contents = await JSON.stringify(loan_data);
await outfile.write(contents);
await outfile.close();
}
catch(error) {
}
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
delete_dlg = document.getElementById('delete_dlg');
const edit_delete = document.getElementById('edit_delete');
edit_delete.addEventListener('click', () => { delete_dlg.showModal(); });
const delete_cancel = document.getElementById('delete_cancel');
delete_cancel.addEventListener('click', () => { delete_dlg.close(); });
const delete_yes = document.getElementById('delete_yes');
delete_yes.addEventListener('click', handleDelete);
const save_button = document.getElementById('save_button');
save_button.addEventListener('click', handleSave);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button>
<button id="save_button">Save</button>
<br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
<button id="edit_delete">Delete Loan</button>
</dialog>
<dialog id="delete_dlg">
Are you sure you want to delete the loan?<br>
<br>
<button id="delete_cancel">Cancel</button>
<button id="delete_yes">Yes</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 95-105, 157-158 and 169. Line 169 adds a Save button to the markup. Line 157 gets a reference to that button and line 158 associates the handleSave() function with clicking on that button.
Lines 95-105 define the handleSave() function. Lines 96-102 define a try block that displays the File Picker dialog. Line 97 will display that File Picker dialog and if the user specifies a file for output will return the file handle. Line 98 creates the outfile object which is a writable object that is used to write to the physical file. Line 99 gets the string representation for the loan_data array and line 100 writes that to outfile. Finally, on line 101 does the flushing to write to the physical file. Lines 103-104 define the catch block. This would come into play if the user hits Cancel instead of specifying an output file. In that case, the program does not need to do anything.
Here is a screen shot showing the loan data table right before saving to the file test.json:
Here is the contents of test.json that was produced:
[{"rate":5.25,"years":5,"amt_borrowed":35000,"pmt":664.5094345199329},{"rate":5.5,"years":5,"amt_borrowed":35000,"pmt":668.540676012372},{"rate":5.75,"years":5,"amt_borrowed":35000,"pmt":672.5868873771335}]
Opening the JSON file
Now that we can save the loan data in a JSON file, we need to be able to read in that file, and populate our loan table with that data. Here is the new version of the app that does that.
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let delete_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function handleDelete() {
const id = loan_id; // already stored by clicking on a table row
loan_data.splice(id, 1); // remove 1 element at position id
const loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
delete_dlg.close();
edit_dlg.close();
}
async function handleSave() {
try {
const handle = await window.showSaveFilePicker();
const outfile = await handle.createWritable();
const contents = await JSON.stringify(loan_data);
await outfile.write(contents);
await outfile.close();
}
catch(error) {
}
}
async function handleOpen() {
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
loan_data = JSON.parse(contents);
localStorage.setItem("loan_data", contents);
updateTable();
}
catch(error) {
}
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
delete_dlg = document.getElementById('delete_dlg');
const edit_delete = document.getElementById('edit_delete');
edit_delete.addEventListener('click', () => { delete_dlg.showModal(); });
const delete_cancel = document.getElementById('delete_cancel');
delete_cancel.addEventListener('click', () => { delete_dlg.close(); });
const delete_yes = document.getElementById('delete_yes');
delete_yes.addEventListener('click', handleDelete);
const save_button = document.getElementById('save_button');
save_button.addEventListener('click', handleSave);
const open_button = document.getElementById('open_button');
open_button.addEventListener('click', handleOpen);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button>
<button id="save_button">Save</button>
<button id="open_button">Open</button>
<br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
<button id="edit_delete">Delete Loan</button>
</dialog>
<dialog id="delete_dlg">
Are you sure you want to delete the loan?<br>
<br>
<button id="delete_cancel">Cancel</button>
<button id="delete_yes">Yes</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 107-118, 172-173 and 185. Line 185 adds the Open button to the markup. Line 172 gets a reference to this button and line 173 associates the handleOpen() function with clicking on this button.
Lines 107-118 define the handleOpen() function. Lines 108-115 define the try block that will display the File Picker (line 109). If the user selects a file for opening an array of file handles is returned. Line 109 gets the first of these handles and on line 110 this handle is used to get the file object. The file object is connected to the physical input file so that on line 111, the text() method will read the contents of that file into the contents variable. Line 112 uses JSON.parse() to convert the contents into a JavaScript object, in this case the loan_data array. Line 113 stores the contents of the input file in localStorage so that localStorage is updated. Finally line 114 calls updateTable() to update the HTML table.
I tested this out by deleting all the loans in the table (one by one) and then opened the file test.json to restore the loan data.
Clearing localStorage and the loan table
It is not convenient to clear the data table one row at a time, so we will provide a way to clear the entire table at once. This can be done by clearing localStorage and setting the loan_data array to an empty array. Then, just calling updateTable() will clear the HTML table.
Here is the new version of the app that will provide this capability:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let delete_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function handleDelete() {
const id = loan_id; // already stored by clicking on a table row
loan_data.splice(id, 1); // remove 1 element at position id
const loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
delete_dlg.close();
edit_dlg.close();
}
async function handleSave() {
try {
const handle = await window.showSaveFilePicker();
const outfile = await handle.createWritable();
const contents = await JSON.stringify(loan_data);
await outfile.write(contents);
await outfile.close();
}
catch(error) {
}
}
async function handleOpen() {
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
loan_data = JSON.parse(contents);
localStorage.setItem("loan_data", contents);
updateTable();
}
catch(error) {
}
}
function handleClear() {
localStorage.clear();
loan_data = [];
updateTable();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt);
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
delete_dlg = document.getElementById('delete_dlg');
const edit_delete = document.getElementById('edit_delete');
edit_delete.addEventListener('click', () => { delete_dlg.showModal(); });
const delete_cancel = document.getElementById('delete_cancel');
delete_cancel.addEventListener('click', () => { delete_dlg.close(); });
const delete_yes = document.getElementById('delete_yes');
delete_yes.addEventListener('click', handleDelete);
const save_button = document.getElementById('save_button');
save_button.addEventListener('click', handleSave);
const open_button = document.getElementById('open_button');
open_button.addEventListener('click', handleOpen);
const clear_button = document.getElementById('clear_button');
clear_button.addEventListener('click', handleClear);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button>
<button id="save_button">Save</button>
<button id="open_button">Open</button>
<button id="clear_button">Clear data</button>
<br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
<button id="edit_delete">Delete Loan</button>
</dialog>
<dialog id="delete_dlg">
Are you sure you want to delete the loan?<br>
<br>
<button id="delete_cancel">Cancel</button>
<button id="delete_yes">Yes</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
The new lines are 120-124, 180-181 and 194. Line 194 adds the Clear button to the markup. Line 180 gets a reference to that button and line 181 uses that reference to associate the handleClear() function with clicking on that button.
Lines 120-124 define the handleClear() function. Line 121 clears localStorage. Line 122 sets the loan_data array to be empty. Line 123 calls the updateTable() function to clear the data table.
Here is a screen shot showing the table being cleared by hitting the Clear data button.
The next screen shot shows the table being restored after using Open to load test.json:
Formatting the monthly payment
Finally, I formatted the monthly payment that goes into the loan table so that it uses only 2 places after the decimal point. Here is the final version of the app that does this:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', init);
let loan_data = [];
let add_dlg;
let edit_dlg;
let delete_dlg;
let loan_id;
function removeChildren(elem) {
while (elem.children.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
class Loan {
constructor(rate, years, amt_borrowed) {
this.rate = rate;
this.years = years;
this.amt_borrowed = amt_borrowed;
const r = (this.rate/100)/12; // convert to percent and monthly rate
const n = this.years*12; // 12 payments per year
const A = this.amt_borrowed;
let num = A*r*(1 + r)**n;
let denom = (1 + r)**n - 1;
this.pmt = num/denom;
}
}
function showAddDialog() {
add_dlg.showModal();
}
function closeAddDialog() {
add_dlg.close();
}
function handleAddOk() {
const rate_box = document.getElementById('rate_box');
const years_box = document.getElementById('years_box');
const amt_box = document.getElementById('amt_box');
let rate = Number(rate_box.value);
let years = Number(years_box.value);
let amt = Number(amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data.push(l1);
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
add_dlg.close();
}
function showEditDialog(event) {
console.log(event.currentTarget);
const index = Number(event.currentTarget.id);
loan_id = index;
console.log(loan_data[index]);
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
const loan = loan_data[index];
edit_rate_box.value = loan.rate;
edit_years_box.value = loan.years;
edit_amt_box.value = loan.amt_borrowed;
edit_dlg.showModal();
}
function handleEditOk() {
const edit_rate_box = document.getElementById('edit_rate_box');
const edit_years_box = document.getElementById('edit_years_box');
const edit_amt_box = document.getElementById('edit_amt_box');
let rate = Number(edit_rate_box.value);
let years = Number(edit_years_box.value);
let amt = Number(edit_amt_box.value);
let l1 = new Loan(rate, years, amt);
loan_data[loan_id] = l1;
let loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
edit_dlg.close();
}
function handleDelete() {
const id = loan_id; // already stored by clicking on a table row
loan_data.splice(id, 1); // remove 1 element at position id
const loan_data_str = JSON.stringify(loan_data);
localStorage.setItem("loan_data", loan_data_str);
updateTable();
delete_dlg.close();
edit_dlg.close();
}
async function handleSave() {
try {
const handle = await window.showSaveFilePicker();
const outfile = await handle.createWritable();
const contents = await JSON.stringify(loan_data);
await outfile.write(contents);
await outfile.close();
}
catch(error) {
}
}
async function handleOpen() {
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
loan_data = JSON.parse(contents);
localStorage.setItem("loan_data", contents);
updateTable();
}
catch(error) {
}
}
function handleClear() {
localStorage.clear();
loan_data = [];
updateTable();
}
function updateTable() {
console.log(loan_data);
const loan_tbody = document.getElementById('loan_tbody');
removeChildren(loan_tbody);
for (let i = 0; i < loan_data.length; i++) {
let loan = loan_data[i];
let tr = document.createElement('tr');
tr.setAttribute("id", i);
tr.addEventListener('click', showEditDialog);
let td = document.createElement('td');
const rate = document.createTextNode(loan.rate);
td.appendChild(rate);
tr.appendChild(td);
const years = document.createTextNode(loan.years);
td = document.createElement('td');
td.appendChild(years);
tr.appendChild(td);
const amt_borrowed = document.createTextNode(loan.amt_borrowed);
td = document.createElement('td');
td.appendChild(amt_borrowed);
tr.append(td);
const pmt = document.createTextNode(loan.pmt.toFixed(2));
td = document.createElement('td');
td.appendChild(pmt);
tr.appendChild(td);
loan_tbody.appendChild(tr);
}
}
function init() {
console.log('init called');
const add_button = document.getElementById('add_button');
add_button.addEventListener('click', showAddDialog);
add_dlg = document.getElementById('add_dlg');
const add_cancel = document.getElementById('add_cancel');
add_cancel.addEventListener('click', closeAddDialog);
const add_ok = document.getElementById('add_ok');
add_ok.addEventListener('click', handleAddOk);
edit_dlg = document.getElementById('edit_dlg');
const edit_cancel = document.getElementById('edit_cancel');
edit_cancel.addEventListener('click', () => { edit_dlg.close(); });
const edit_ok = document.getElementById('edit_ok');
edit_ok.addEventListener('click', handleEditOk);
delete_dlg = document.getElementById('delete_dlg');
const edit_delete = document.getElementById('edit_delete');
edit_delete.addEventListener('click', () => { delete_dlg.showModal(); });
const delete_cancel = document.getElementById('delete_cancel');
delete_cancel.addEventListener('click', () => { delete_dlg.close(); });
const delete_yes = document.getElementById('delete_yes');
delete_yes.addEventListener('click', handleDelete);
const save_button = document.getElementById('save_button');
save_button.addEventListener('click', handleSave);
const open_button = document.getElementById('open_button');
open_button.addEventListener('click', handleOpen);
const clear_button = document.getElementById('clear_button');
clear_button.addEventListener('click', handleClear);
if (localStorage.getItem('loan_data') !== null) {
loan_data = JSON.parse(localStorage.getItem('loan_data'));
updateTable();
}
}
</script>
</head>
<body>
<h1>Loan data</h1>
<button id="add_button">Add a loan</button>
<button id="save_button">Save</button>
<button id="open_button">Open</button>
<button id="clear_button">Clear data</button>
<br>
<dialog id="add_dlg">
Enter annual interest rate:
<input type="text" id="rate_box"><br>
Enter years for loan:
<input type="text" id="years_box"><br>
Enter amount borrowed:
<input type="text" id="amt_box"><br>
<br>
<button id="add_cancel">Cancel</button>
<button id="add_ok">Ok</button>
</dialog>
<dialog id="edit_dlg">
Annual interest rate:
<input type="text" id="edit_rate_box"><br>
Years for loan:
<input type="text" id="edit_years_box"><br>
Amount borrowed:
<input type="text" id="edit_amt_box"><br>
<br>
<button id="edit_cancel">Cancel</button>
<button id="edit_ok">Update</button>
<button id="edit_delete">Delete Loan</button>
</dialog>
<dialog id="delete_dlg">
Are you sure you want to delete the loan?<br>
<br>
<button id="delete_cancel">Cancel</button>
<button id="delete_yes">Yes</button>
</dialog>
<table border="1">
<thead>
<tr>
<th>Annual Interest Rate</th>
<th>Years for Loan</th>
<th>Amount Borrowed</th>
<th>Monthly Payment</th>
</tr>
</thead>
<tbody id="loan_tbody"></tbody>
</table>
</body>
</html>
Line 147 was modified to use toFixed(w) to round the payment to two decimal places.
Here is a screen shot showing the loan table with this change: