skip to Main Content

I am facing a problem with the Grand Total calculation in a dynamic HTML table where rows can be added, removed, and quantities can be adjusted. The issue arises when a new row is added or a row is removed; the Grand Total does not update correctly unless the quantity is manually increased.

Here’s a brief overview of the functionality:

  • Products can be added to the table from a datalist.
  • Quantity input for each product allows adjustment.
  • Rows can be removed with a "Remove" button.

The Grand Total is expected to update dynamically with each modification to the table, but there seems to be an issue in the JavaScript code that handles the calculation.

// Initial check when the page loads
document.addEventListener("DOMContentLoaded", function() {
  document.getElementById("productTable").style.display = "none";
});

function addProductRow() {
  document.getElementById("productTable").style.display = "table";
  var productInput = document.getElementById("product");
  var productName = productInput.value;

  if (productName.trim() !== "") {
    var productDataList = document.getElementById("products");
    var selectedOption = [...productDataList.options].find(option => option.value === productName);
    updateGrandTotal();

    if (selectedOption) {
      var productPrice = selectedOption.getAttribute("data-price");
      var tableBody = document.getElementById("productTable").getElementsByTagName('tbody')[0];

      var row = tableBody.insertRow();
      row.innerHTML = `
                    <td>${productName}</td>
                    <td>$${productPrice}</td>
                    <td>
                        <input type="number" name="quantity" min="1" value="1" oninput="updateSubtotal(this); updateGrandTotal();">
                    </td>
                    <td class="subtotal">$${productPrice}</td>
                    <td>
                        <button class="remove-button" onclick="removeProductRow(this)">Remove</button>
                    </td>
                `;

      productInput.value = ""; // Clear the product input after adding a row
      updateGrandTotal();
    }
  }
}

function removeProductRow(button) {
  var row = button.parentElement.parentElement;
  row.parentElement.removeChild(row);
  updateGrandTotal();

  //Check if there are no products
  var tableBody = document.getElementById("productTable").getElementsByTagName('tbody')[0];
  if (tableBody.rows.length < 2) {
    document.getElementById("productTable").style.display = "none";
  }

  updateGrandTotal();
}

function updateSubtotal(input) {
  var quantity = parseInt(input.value);
  var unitPriceCell = input.parentElement.previousElementSibling;
  var unitPrice = parseFloat(unitPriceCell.textContent.replace('$', ''));
  var row = input.parentElement.parentElement;

  var subtotalCell = row.querySelector('.subtotal');
  var subtotal = quantity * unitPrice;
  subtotalCell.textContent = `$${subtotal}`;
  updateGrandTotal();
}

function updateGrandTotal() {
  var tableBody = document.getElementById("productTable").getElementsByTagName('tbody')[0];
  var rows = tableBody.getElementsByTagName('tr');
  var grandTotal = 0;

  for (var i = 0; i < rows.length - 1; i++) {
    //console.log("rows length :", rows.length)
    //console.log("i value:", i); // Log the value

    var subtotalCell = rows[i].querySelector('.subtotal');
    //console.log("Sub Total Cell :", rows.length)

    grandTotal += parseInt(subtotalCell.textContent.replace('$', '')) || 0;

    //console.log("grand total :", grandTotal)

    //console.log("")

  }

  var grandTotalRow = document.getElementById("grandTotalRow");
  if (grandTotalRow) {
    grandTotalRow.parentElement.removeChild(grandTotalRow);
  }

  grandTotalRow = tableBody.insertRow();
  grandTotalRow.id = "grandTotalRow";
  grandTotalRow.className = "grand-total-row";
  grandTotalRow.innerHTML = `
            <td colspan="2"></td>
            <td align="right"><strong>Grand Total:</strong></td>
            <td class="subtotal" id="grandTotalValue">$${grandTotal}</td>
            <td></td>
        `;
}
<form>
  <table id="productTable">
    <thead>
      <tr>
        <th>Product Name</th>
        <th>Unit Price</th>
        <th>Quantity</th>
        <th>Sub-Total</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  <label for="product">Product name:</label>
  <input list="products" name="product" id="product" oninput="addProductRow()">
  <datalist id="products">
    <option value="Product 1" data-price="25">
    <option value="Product 2" data-price="30">
    <option value="Product 3" data-price="15">
    <option value="Product 4" data-price="22">
  </datalist>

  <input type="submit">
</form>

3

Answers


  1. Chosen as BEST ANSWER

    The answer by @mplungjan worked. The Grand total was updated properly. https://stackoverflow.com/a/77598801/14238946

    But it introduced a bug for sub-totals where it's not responsive to value increase or decrease. Modifying the updateSubtotal part solved it. Instead of getting the unit price from the .subtotal cell, it should be obtained from the corresponding product row.-

    const updateSubtotal = (input) => {
                const row = input.closest('tr');
                const productName = row.querySelector('td:first-child').textContent;
                const selectedOption = [...productDataList.options].find(option => option.value === productName);
                if (selectedOption) {
                    const unitPrice = +selectedOption.getAttribute("data-price");
                    const quantity = parseInt(input.value);
                    const subtotalCell = row.querySelector('.subtotal');
                    const subtotal = quantity * unitPrice;
                    subtotalCell.textContent = `$${subtotal}`;
                    updateGrandTotal();
                }
            };
    

  2. I strongly recommend delegation

    Here is a rewrite. Note I moved the grandTotal to a static tfoot

    document.addEventListener("DOMContentLoaded", function() {
      const table = document.getElementById("productTable")
      table.hidden = true;
      const tableBody = document.querySelector("#productTable tbody");
      const productDataList = document.getElementById("products");
      const productInput = document.getElementById("product");
      const grandTotal = document.getElementById("grandTotalValue");
      const updateGrandTotal = () => {
        const rows = tableBody.querySelectorAll('tr');
        table.hidden = rows.length === 0;
        if (rows.length === 0) return;
        const total = [...rows].map(row => +row.querySelector('.subtotal').textContent.trim().slice(1)).reduce((a, b) => a + b)
        grandTotal.textContent = `${total.toFixed(2)}`;
      };
      const updateSubtotal = (input) => {
        var quantity = parseInt(input.value);
        const row = input.closest('tr');
        var unitPrice = +row.querySelector('.subtotal').textContent.trim().slice(1);
        var subtotalCell = row.querySelector('.subtotal');
        var subtotal = quantity * unitPrice;
        subtotalCell.textContent = `$${subtotal}`;
        updateGrandTotal();
      };
    
      const addProductRow = () => {
        table.hidden = false;
        var productName = productInput.value;
        if (productName.trim() === "") return;
        var selectedOption = [...productDataList.options].find(option => option.value === productName);
        if (selectedOption) {
          var productPrice = selectedOption.getAttribute("data-price");
          tableBody.innerHTML += `<tr>
            <td>${productName}</td>
            <td>$${productPrice}</td>
            <td><input type="number" name="quantity" min="1" value="1" "></td>
            <td class="subtotal">$${productPrice}</td>
            <td><button class="remove-button">Remove</button></td>
          </tr>`;
          productInput.value = ""; // Clear the product input after adding a row
        }
      };
      tableBody.addEventListener('input', (e) => {
        const tgt = e.target;
        if (tgt.matches("input[name=quantity]")) {
          updateSubtotal(tgt);
          updateGrandTotal();
        }
      });
      tableBody.addEventListener('click', (e) => {
        const tgt = e.target.closest('button.remove-button');
        if (!tgt) return;
        tgt.closest('tr').remove();
        updateGrandTotal();
      });
      productInput.addEventListener('change', (e) => {
        console.log('adding')
        addProductRow();
        updateGrandTotal();
      });
    });
    <form>
      <table id="productTable">
        <thead>
          <tr>
            <th>Product Name</th>
            <th>Unit Price</th>
            <th>Quantity</th>
            <th>Sub-Total</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
        <tfoot>
          <td colspan="2"></td>
          <td align="right"><strong>Grand Total:</strong></td>
          <td class="subtotal" id="grandTotalValue">$0.00</td>
        </tfoot>
      </table>
      <label for="product">Product name:</label>
      <input list="products" name="product" id="product">
      <datalist id="products">
        <option value="Product 1" data-price="25">
        <option value="Product 2" data-price="30">
        <option value="Product 3" data-price="15">
        <option value="Product 4" data-price="22">
      </datalist>
    
      <input type="submit">
    </form>
    Login or Signup to reply.
  3. The way you’re changing the total is the problem with your grand total computation. Every time the updateGrandTotal() function is run, a new row for the Grand Total is created. Updates should be made to the current Grand Total row instead.

    function updateGrandTotal() {
      var tableBody = document.getElementById("productTable").getElementsByTagName('tbody')[0];
      var rows = tableBody.getElementsByTagName('tr');
      var grandTotal = 0;
    
      for (var i = 0; i < rows.length - 1; i++) {
        var subtotalCell = rows[i].querySelector('.subtotal');
        grandTotal += parseFloat(subtotalCell.textContent.replace('$', '')) || 0;
      }
    
      // Find the existing Grand Total row
      var grandTotalRow = document.getElementById("grandTotalRow");
    
      if (grandTotalRow) {
        // Update the Grand Total value in the existing row
        var grandTotalValueCell = grandTotalRow.querySelector('#grandTotalValue');
        grandTotalValueCell.textContent = `$${grandTotal}`;
    
        // Remove the row if the Grand Total becomes zero
        if (grandTotal === 0) {
          grandTotalRow.parentElement.removeChild(grandTotalRow);
        }
      } else if (grandTotal > 0) {
        // If Grand Total row doesn't exist, create a new one
        grandTotalRow = tableBody.insertRow();
        grandTotalRow.id = "grandTotalRow";
        grandTotalRow.className = "grand-total-row";
        grandTotalRow.innerHTML = `
          <td colspan="2"></td>
          <td align="right"><strong>Grand Total:</strong></td>
          <td class="subtotal" id="grandTotalValue">$${grandTotal}</td>
          <td></td>
        `;
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search