The Code

                    
function runMortgageCalculator() {

    // Get Variables
    let mortgageVariables = getMortgageVariables()

    // Calculate the mortagge loan values from Variables
    let mortgageCalcObject = calculateMortgage(mortgageVariables);

    displayMortgageSummary(mortgageCalcObject.summary);

    displayAmortTable(mortgageCalcObject.amortizationArray);

    // Get calculations from local storage
    let mortgageCalcsArray = getCalculation();
    
    if (mortgageCalcsArray.length > 0) {
        displayHistory(mortgageCalcsArray);
    }

    // Update array with new mortgage calculation
    mortgageCalcsArray.push(mortgageCalcObject);

    // Save new updated array to local storage
    saveCalculation(mortgageCalcsArray);

}

// Grab the variables from the form and change to numbers to calculate the mortgage
function getMortgageVariables() {

    // Get HTML Mortgage Calculator Form Element
    let newMortgageCalcForm = document.getElementById('newMortgageCalcForm');
    let mortgageFormVariables = new FormData(newMortgageCalcForm);

    // Create a mortgage variable object from the Mortgage Calculator Form
    let newMortgageVariables = Object.fromEntries(mortgageFormVariables.entries());

    // Change values in object from string to numbers
    newMortgageVariables.loan = parseInt(newMortgageVariables.loan);
    newMortgageVariables.term = parseInt(newMortgageVariables.term);
    newMortgageVariables.rate = parseFloat(newMortgageVariables.rate);

    let loan = newMortgageVariables.loan;
    let term = newMortgageVariables.term;
    let rate = newMortgageVariables.rate;

    if (!isNaN(loan) 
        && !isNaN(term) 
        && !isNaN(rate)
        && loan > 0
        && term > 0
        && rate >= 0 ) {
            return newMortgageVariables;
    } else {

        // Uh oh!
        Swal.fire ({
            icon: 'error',
            backdrop: false,
            title: 'Oops!',
            text: 'Please enter valid integers. Check term and loan are greater than zero and rate is equal to or greater than zero.'
        });
    }
}

// Calculates values to put on amortization table and returns it in array of objects
function calculateMortgage(mortgageVariables) {

    let loan = mortgageVariables.loan;
    let term = mortgageVariables.term;
    let rate = mortgageVariables.rate;

    // Calculate Monthly Payment
    let monthlyPayment = loan * (rate / 1200) / (1 - Math.pow((1 + rate/1200), -term));

    // Array to hold all the objects for each monthly calculation
    let amortizationArray = [];

    // Initialize monthly object 
    let monthlyMortgageCalculation = {
        month: 0,
        monthlyPayment: 0,
        monthlyPrincipal: 0,
        monthlyInterest: 0,
        totMonthlyPrincipal: 0,
        totMonthlyInterest: 0,
        monthlyBalance: loan
    };

    // Calculate values and put them into calculation object for that month and add to array
    for (let i=0; i <= term; i++) {

        if (i > 0 ) {

            // Set the month
            let month = i;
                    
            // Calculate the monthly interest Payment
            let remaingingBalance = amortizationArray[i-1].monthlyBalance
            let monthlyInterest = remaingingBalance * rate / 1200;

            // Calculate the monthly principal payment
            let monthlyPrincipal = monthlyPayment - monthlyInterest;

            // Calculate the total monthly principal up to that month
            let totMonthlyPrincipal = amortizationArray[i-1].totMonthlyPrincipal + monthlyPrincipal ;

            // Calculate the total monthly interest up to that month
            let totMonthlyInterest = amortizationArray[i-1].totMonthlyInterest + monthlyInterest;

            // Calculate the remaining balance
            remaingingBalance -= monthlyPrincipal;

            // Put all calculations for month into object
            monthlyMortgageCalculation = {
                month: month,
                monthlyPayment: monthlyPayment,
                monthlyPrincipal: monthlyPrincipal,
                monthlyInterest: monthlyInterest,
                totMonthlyPrincipal: totMonthlyPrincipal,
                totMonthlyInterest: totMonthlyInterest,
                monthlyBalance: remaingingBalance
            };

        }

        // Add calculation for month to array
        amortizationArray.push(monthlyMortgageCalculation);
    }

    // Get the last month in the amortization array
    let lastMonth = amortizationArray[term];

    // Put summary in object
    let summary = {
        monthlyPayment: lastMonth.monthlyPayment,
        totPrincipal: lastMonth.totMonthlyPrincipal,
        totInterest: lastMonth.totMonthlyInterest,
        totCost: lastMonth.totMonthlyPrincipal + lastMonth.totMonthlyInterest
    }

    // Make one object that holds summary object and amortization array
    let mortgageCalcObject = {
        mortgageVariables: mortgageVariables,
        summary: summary,
        amortizationArray: amortizationArray
    }

    return mortgageCalcObject;
}

// Display value summary
function displayMortgageSummary(mortgageCalcSummary) {

    let summary = mortgageCalcSummary;
    
    // Could use toLocaleString instead
    let dollarUSLocal = Intl.NumberFormat('en-US', {
        style: "currency",
        currency: "USD"
    });

    let monthlyPayment = document.getElementById('monthlyPayment');
    monthlyPayment.textContent = dollarUSLocal.format(summary.monthlyPayment);

    let totPrincipal = document.getElementById('totPrincipal');
    totPrincipal.textContent = dollarUSLocal.format(summary.totPrincipal);

    let totInterest = document.getElementById('totInterest');
    totInterest.textContent = dollarUSLocal.format(summary.totInterest);

    let totCost = document.getElementById('totCost');
    totCost.textContent = dollarUSLocal.format(summary.totCost);

}

// Display values from array on the amortization table
function displayAmortTable(amortizationArray) {

    let dollarUSLocal = Intl.NumberFormat('en-US', {
        style: "currency",
        currency: "USD"
    });

    // Get the table body element to put the rows in
    const amortTable = document.getElementById('amortizationSchedule');

    // Rest table
    amortTable.innerHTML = '';

    // Get the amortization table row template
    const amortRowTemplate = document.getElementById('amortRowTemplate');

    // Make each row for the table and put the data in the table
    for(i = 1; i < amortizationArray.length; i++) {

        let monthlyMortgage = amortizationArray[i];

        let amortRow = amortRowTemplate.content.cloneNode(true);

        // Could use 'querySelectorAll' to get all the 'td' as an array and index them accordingly put content in
        let month = amortRow.querySelector('.month');
        month.textContent = monthlyMortgage.month;

        let monthlyPayment = amortRow.querySelector('.monthlyPayment');
        monthlyPayment.textContent = dollarUSLocal.format(monthlyMortgage.monthlyPayment);

        let monthlyPrincipal = amortRow.querySelector('.monthlyPrincipal');
        monthlyPrincipal.textContent = dollarUSLocal.format(monthlyMortgage.monthlyPrincipal);

        let monthlyInterest = amortRow.querySelector('.monthlyInterest');
        monthlyInterest.textContent = dollarUSLocal.format(monthlyMortgage.monthlyInterest);

        let totMonthlyInterest = amortRow.querySelector('.totMonthlyInterest');
        totMonthlyInterest.textContent = dollarUSLocal.format(monthlyMortgage.totMonthlyInterest);

        let monthlyBalance = amortRow.querySelector('.monthlyBalance');
        // Can use Math.abs in above monthlyBalance instead of if statement to make sure it only returns positive values
        if (monthlyMortgage.monthlyBalance < 0 ) {

            monthlyBalance.textContent = dollarUSLocal.format(0);
        }

        amortTable.appendChild(amortRow);
    }
}

// Get calculations that are stored in local storage
function getCalculation() {
    // Get calculations from local storage
    let mortgageCalcsJson = localStorage.getItem('rpc-mortgageCalcs');

    // Initialize calcs if it hasn't been created before
    let storedMortgageCalcs = [];

    if (mortgageCalcsJson != null) {
        storedMortgageCalcs = JSON.parse(mortgageCalcsJson);
    }

    return storedMortgageCalcs;
}

// Display past calculations
function displayHistory(mortgageCalcsArray) {

    const historyCardTemplate = document.getElementById('historyCardTemplate');

    const historyCardDiv = document.getElementById('historyCardDiv');

    historyCardDiv.textContent = '';

    let dollarUSLocal = Intl.NumberFormat('en-US', {
        style: "currency",
        currency: "USD"
    });

    for (let i = mortgageCalcsArray.length - 1; i >= 0 ; i--) {

        let mortgageCalc = mortgageCalcsArray[i];

        // Grab values to add to card
        let monthlyPaymentHistValue = mortgageCalc.summary.monthlyPayment;
        monthlyPaymentHistValue = dollarUSLocal.format(monthlyPaymentHistValue);
        let loanHistValue = mortgageCalc.mortgageVariables.loan;
        loanHistValue = loanHistValue/1000;
        let termHistValue = mortgageCalc.mortgageVariables.term;
        termHistValue = termHistValue;
        let rateHistValue = mortgageCalc.mortgageVariables.rate;
        rateHistValue = rateHistValue;

        let historyCard = historyCardTemplate.content.cloneNode(true);

        let collapseCard = historyCard.querySelector('.collapse');
        collapseCard.setAttribute('id', `collapse-${i}`);
        
        let collapseBtn = historyCard.querySelector('.collapse .btn-close');
        collapseBtn.setAttribute('data-bs-target', `#collapse-${i}`);
        collapseBtn.setAttribute('aria-controls',`collapse-${i}`);
        collapseBtn.setAttribute('data-mortgageCalcsArray-id',`${i}`);

        let monthlyPaymentHist = historyCard.querySelector('.monthlyPaymentHist');
        monthlyPaymentHist.textContent = monthlyPaymentHistValue;

        let loanHist = historyCard.querySelector('.loanHist');
        loanHist.textContent = loanHistValue;
        
        let termHist = historyCard.querySelector('.termHist');
        termHist.textContent = termHistValue; 

        let rateHist = historyCard.querySelector('.rateHist');
        rateHist.textContent = rateHistValue; 

        historyCardDiv.appendChild(historyCard);
    }

}

// Save calculation in local storage
function saveCalculation(mortgageCalcsArray) {

    // Turn mortgage object into string
    let mortgageCalcsJson = JSON.stringify(mortgageCalcsArray);

    localStorage.setItem('rpc-mortgageCalcs', mortgageCalcsJson);
}

// Delete the saved calculation if the close button is pressed on the history cards
function deleteCalculation(closeBtn) {

    // Get number that identifies history card
    let arrayNum = parseInt(closeBtn.getAttribute('data-mortgageCalcsArray-id'));

    // Get calculations from local storage
    let mortgageCalcsArray = getCalculation();
    
    // Decalare new array variable to move over the objects that are not deleted
    let newMortgageCalcsArray = [];

    for (let i = 0; i < mortgageCalcsArray.length; i++) {

        if (i != arrayNum) {
        // Move over saved calculation
        newMortgageCalcsArray.push(mortgageCalcsArray[i]);
        }
    }

    // Save new updated array to local storage
    saveCalculation(newMortgageCalcsArray);

}
                    
                

TL;DR

Start small whether in learning or building. The pieces (knowledge / functions) will start stacking. This is a culmination of knowledge gained from all previous challenges.

Code Explanation

Green Grove Mortgage was created with the following functions:

runMortgageCalculator is the entry point of this application. An EventListener is used to watch for the submit event and call this function. The submit event happens when the "Calculate" button is clicked. The button is of type="submit" so that all the values from the form can be grabbed as an object instead of individually grabbing the value of each element in the form.

getMortgageVariables grabs the values entered by the user as an object with Object.fromEntries. Since everything that comes to JavaScript from the HTML is a string, the properties in the object need to be changed to integers or floats using parseInt or parseFloat accordingly. If the inputs are not valid a sweet alert is used to notify the user.

calculateMortgage has mortgageVariables as a parameter which is an object with properties that have values that were entered by the user. The values are then used to calculate all the properties in the monthlyMortgageCalculation and summary objects. In order to calculate the mortgage properties for each month of term, the amortizationArray variable is created to hold an array of monthlyMortgageCalculation objects. The mortgageVariables, summary, and amortizationArray are then assigned to properties in the mortgageCalcObject so all the information related to the calculation can stay together and be easily accessed.

displayMortgageSummary takes the properties of the summary object and displays them in the "Your Monthly Payments" section of the app. Before the properties get displayed, they are formated to US dollars using Intl.NumberFormat.

displayAmortTable takes the amortizationArray which holds the monthly mortgage calculations and displays them on the page as a table. Before the properties are displayed on the page, they are formated to US dollars using Intl.NumberFormat. A template tag in the HTML is used for a row of the table to easily recreate a row based on the properties of each monthlyMortgageCalculation object in the amortizationArray.

getCalculation accesses the localStorage and gets previous mortgage calculations if there are any.

displayHistory A template tag in the HTML is used for the history card to easily recreate a card which shows the monthly payment of previous mortgage calculations along with the values entered by the user. Before the properties are displayed on the card, they are formated to US dollars using Intl.NumberFormat. Each card is assigned custom attributes that work with Bootstrap's collapse feature so that the card can be closed accordingly when the "X" button is clicked.

saveCalculation converts the object mortgageCalcsArray parameter intoJSON and assigns it to a variable in the localStorage.

deleteCalculation has a button element as a parameter. This is the close button on the history card that the user clicked to close. It then uses getCalculation to grab the current information in the localStorage and copies all the saved mortgageCalcObject objects to a new array except for the one tied to the history card that was closed. The new array is then saved to the localStorage.

What I learned

  • Use Object.fromEntries to easily grab values from a form as an object
  • Use localStorage.clear to clear the local storage or just assign the variables saved to empty values

Improvements

  • When user clicks on history card, it populates the page fields based on the values from that card
  • Button to see all past history cards
  • Labeling so user knows what the icons are