// getValues() collects user input and directs the flow of data
function getValues() {
// grab inputs
let principal = document.getElementById('principal').value;
let term = document.getElementById('term').value;
let rate = document.getElementById('rate').value;
// validate inputs
principal = parseFloat(principal); // money can be float, but only 2 decimals
term = parseInt(term); // must be int - i.e. fixed # of months
rate = parseFloat(rate); // typically is a float
if (Number.isNaN(principal) || Number.isNaN(term) || Number.isNaN(rate)) {
// one of the inputs is wrong
let errorMsg = "Please only use numbers for:<br>";
if (Number.isNaN(principal)) errorMsg += "<br>Loan Amount";
if (Number.isNaN(term)) errorMsg += "<br>Term";
if (Number.isNaN(rate)) errorMsg += "<br>Interest Rate";
Swal.fire({
backdrop: false,
title: "Error!",
html: errorMsg
});
return;
}
principal = principal.toFixed(2); // if we call this before validating, it will always be a number
// give inputs a formatted placeholder to look nicer
document.getElementById('principal').value = '';
document.getElementById('term').value = '';
document.getElementById('rate').value = '';
document.getElementById('principal').placeholder = formatCurrency(principal);
document.getElementById('term').placeholder = term;
document.getElementById('rate').placeholder = `${rate}%`;
let paymentData = calculatePayments(principal, rate, term);
displayData(paymentData);
}
// calculatePayments() takes in data and generates values for each month
// of the loan term as well as totals for the overview
function calculatePayments(principal, rate, term) {
let totalMonthlyPayment = (principal * (rate / 1200)) / (1 - Math.pow((1 + (rate / 1200)), -term));
let totalCost = totalMonthlyPayment * term;
let monthlyPaymentArr = [
// placeholder object for calculations
{
month: 0,
monthlyPayment: totalMonthlyPayment,
principalPayment: 0,
interestPayment: 0,
totalInterestPaid: 0,
balance: principal,
}];
for (let month = 1; month <= term; month++) {
let prevBalance = monthlyPaymentArr[month - 1].balance;
let prevTotalInterest = monthlyPaymentArr[month - 1].totalInterestPaid;
let interestPayment = prevBalance * (rate / 1200);
let principalPayment = totalMonthlyPayment - interestPayment;
let balance = prevBalance - principalPayment;
let totalInterest = prevTotalInterest + interestPayment;
if (balance < 0) balance = 0;
monthlyPaymentArr.push({
month: month,
monthlyPayment: totalMonthlyPayment,
principalPayment: principalPayment,
interestPayment: interestPayment,
totalInterestPaid: totalInterest,
balance: balance
});
}
// remove placeholder object
monthlyPaymentArr.shift();
// totals for displayTotals()
let totals = {
monthlyPayments: totalMonthlyPayment,
totalPrincipal: principal,
totalInterest: totalCost - principal,
totalCost: totalCost
}
return {
totals: totals,
payments: monthlyPaymentArr
}
}
// separates the data and sends it to each view
function displayData(data) {
// display totals to provide a summary
displayTotals(data.totals);
// display each month of payments in a table
displayPayments(data.payments);
}
// displayTotals() presents the overview at the top of the page
function displayTotals(totals) {
// get stats
let totalMonthly = totals.monthlyPayments;
let totalPrincipal = totals.totalPrincipal;
let totalInterest = totals.totalInterest;
let totalCost = totals.totalCost;
// grab template
let statsTemplate = document.getElementById('totals-template');
let container = document.getElementById('totals-container')
// clear container
container.innerHTML = '';
// create a copy
let statsNode = document.importNode(statsTemplate.content, true);
// put values in the copy
statsNode.getElementById('total-principal').textContent = formatCurrency(totalPrincipal);
statsNode.getElementById('total-interest').textContent = formatCurrency(totalInterest);
statsNode.getElementById('total-cost').textContent = formatCurrency(totalCost);
statsNode.getElementById('total-monthly').textContent = formatCurrency(totalMonthly);
// attach it to the page
container.appendChild(statsNode);
}
// displayPayments() presents a table of each month of payments
function displayPayments(payments) {
// grab table row template
let rowTemplate = document.getElementById('payment-row-template');
let paymentTable = document.getElementById('payment-table');
// make the table visible now that we have data to show
document.querySelector('table').classList.remove('d-none');
// clear the table in case it was already populated
paymentTable.innerHTML = '';
// generate a table row for each month of data, matching
// the object keys to 'data-key` in the template
payments.forEach(payment => {
let rowNode = document.importNode(rowTemplate.content, true);
Object.keys(payment).forEach(key => {
let value = payment[key];
if (key != 'month') value = formatCurrency(value);
rowNode.querySelector(`[data-key="${key}"]`).textContent = value;
});
paymentTable.appendChild(rowNode);
});
}
// formatCurrency() takes a number and returns a string formatted as money
// which is used for convenience in the display functions
function formatCurrency(value) {
let formatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' });
return formatter.format(Number(value));
}
The Code
The code is broken into functions to separate concerns in the MVC model.
getValues acts as our controller for the function - it runs when the
user confirms their input with the "calculate" button and collects their inputs,
validates that we received suitable data, and sends it to our Model for calculation.
Once it has received the caluclated data, the model hands that data off to a View function.
calculateValues receives user input of principal, rate,
and term according to their mortgage details. Then, we use a for loop
to calculate each month of payments based upon the length of the loan's term and
pushes an object with that month of information into an array. Finally, calculateValues
returns an object with property totals that contains information for our loan's overview
and property payments for our array containing each month of payment dta.
displayData receives the object that was returned from calculatePayments
and sends the totals off to displayTotals and the array of monthly payments to
displayPayments. displayTotals simply accesses each piece of information
from its argument and uses a template to place it onto the page in our overview window.
displayData uses a template for each row of our table and loops through our array
of monthly payments, then matches the object's property keys to the templates data-key
attribute and attaches that row to our data table.