0

I'm adding table rows dynamically with a button "Add New Line". When clicked, this fires off a jQuery event which creates a new table row containing form fields. The form fields allow numeric values only.

When a value gets entered into the field, it calculates VAT and Line Total amounts. These totals then get appended to the closest() table row <tr>.

The code works fine for the first row. As soon as a new row is added, the code stops working and values are calculated for the second row.

I believe this has something to do with the fact the rows (and form fields) are dynamically created by jQuery and injected into the page, so closest() cant't find them.

Whats the solution to this?

Thanks in advance.

Javascript:

$(document).ready(function () {
    var counter = 2;

    $("#addRow").on("click", function () 
    {
        var newRow = $("<tr>");
        var cols = "";

        cols += '<td>' + counter + '</td>';
        cols += '<td><textarea class="form-control" name="description[' + counter + ']"></textarea></td>';
        cols += '<td><input type="text" class="form-control line-quantity" name="quantity[' + counter + ']" onkeypress="return isNumberKey(event)"></td>';
        cols += '<td><input type="text" class="form-control line-price" name="unit_price[' + counter + ']" onkeypress="return isNumberKey(event)"></td>';
        cols += '<td class="line_vat">£0.00</td>';
        cols += '<td class="line_total">£0.00</td>';
        cols += '<td><input type="button" class="ibtnDel btn btn-md btn-danger"  value="Delete"></td>';
        newRow.append(cols);
        $("table.table-striped").append(newRow);
        counter++;
    });

    $("table.table-striped").on("click", ".ibtnDel", function (event) 
    {
        $(this).closest("tr").remove();
        counter -= 1
    });

    // Update line vat & subtotal
    $(".line-quantity, .line-price").on("blur", function (event)
    {
        var line_qty = 0;
        var line_price = 0;
        var line_subtotal = '0.00';
        var vat = '0.00';
        var line_total = '0.00';

        line_qty = $(this).closest('#quote_table').find('input.line-quantity').val();
        line_price = $(this).closest('#quote_table').find('input.line-price').val();

        // calculate total line price
        line_subtotal = line_price*line_qty;

        // Calculate VAT
        vat = line_subtotal*1.2-line_subtotal;

        // Line total
        line_total = line_subtotal+vat;
        
        // Update view with totals
        $(this).closest('tr').find('td.line_vat').html(vat.toLocaleString('en-US', {style: 'currency',currency: 'GBP'}));
        $(this).closest('tr').find('td.line_total').html(line_total.toLocaleString('en-US', {style: 'currency',currency: 'GBP'}));

        // Calculate totals at end of table

    });
});

function isNumberKey(evt){
    var charCode = (evt.which) ? evt.which : evt.keyCode;
    if (charCode != 46 && charCode > 31 && (charCode < 48 || charCode > 57))
    return false;
    return true;
}

HTML:

<form method="post" action="{{ url('cp/quotes/create') }}">
    {{ csrf_field() }}
    <div class="form-group">
      @if($contacts->isEmpty())
      <a href="/cp/contacts/create">Create first contact</a>
      @else
      <label for="contact">Contact</label>
      <!-- Get the selected contact and fetch properties -->
      <select class="form-control selectpicker show-tick" data-live-search="true" id="contact" name="contact" data-live-search-placeholder="Find contact...">
        <option value="">Select a contact...</option>
      @foreach($contacts as $contact)
        <option data-tokens="{{ $contact->id }}" value="{{ $contact->id }}">{{ $contact->contact_name }} ({{ $contact->first_name }} {{ $contact->last_name }})</option>
      @endforeach
      </select>
      <!-- List contact properties -->
      <div class="btn-group-vertical property_list" role="group" aria-label="Property" id="property_list"></div>
      @endif
    </div>
    <div class="form-group">
      <label class="control-label" for="raised_date">
       Date
      </label>
      <div class="input-group">
        <div class="input-group-addon">
          <i class="fa fa-calendar"></i>  
        </div>
        <input class="form-control" id="raised_date" name="raised_date" type="text"/>
      </div>
    </div>
    <div class="form-group">
      <label class="control-label" for="raised_date">
       Expiry Date
      </label>
      <div class="input-group">
        <div class="input-group-addon">
          <i class="fa fa-calendar"></i>  
        </div>
        <input class="form-control" id="expiry_date" name="expiry_date" type="text"/>
      </div>
    </div>
    <table class="table table-striped" id="quote_table">
      <thead>
        <tr>
          <th scope="col">Item #</th>
          <th scope="col">Description</th>
          <th scope="col">Quantity</th>
          <th scope="col">Unit Price</th>
          <th scope="col">VAT</th>
          <th scope="col">Amount</th>
          <th scope="col"></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>1</td>
          <td><textarea class="form-control" name="description[]"></textarea></td>
          <td><input class="form-control line-quantity" name="quantity[]" type="text" onkeypress="return isNumberKey(event)"></td>
          <td><input class="form-control line-price" name="unit_price[]" type="text" onkeypress="return isNumberKey(event)"></td>
          <td class="line_vat">£0.00</td>
          <td class="line_total">£0.00</td>
          <td></td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td colspan="5" class="text-end">Subtotal</td>
          <td colspan="2" class="subtotal">£0.00</td>
        </tr>
        <tr>
          <td colspan="5" class="text-end h3">Total</td>
          <td colspan="2" class="total h3">£0.00</td>
        </tr>
      </tfoot>
    </table>
    <a class="btn btn-warning addRow" href="#" id="addRow">Add Line +</a>
    <div class="form-group">
      <br /><button type="submit" class="btn btn-success">Save Quote</button>
    </div>
  </form>

Application screenshot:

enter image description here

V4n1ll4
  • 5,973
  • 14
  • 53
  • 92

1 Answers1

1

I tested your code in my own development environment and found that the onblur functionality does not work properly for new dynamic added rows in table. So I searched the web and found this question that could solve your problem. After that I changed your HTML and JavaScript codes as follows:

$(document).ready(function () {

    var counter = 2;

    $("#addRow").on("click", function ()
    {
        var newRow = $("<tr>");
        var cols = "";

        cols += '<td>' + counter + '</td>';
        cols += '<td><textarea class="form-control" name="description[' + counter + ']"></textarea></td>';
        cols += '<td><input type="text" class="form-control line-quantity" name="quantity[' + counter + ']" onkeypress="return isNumberKey(event)"></td>';
        cols += '<td><input type="text" class="form-control line-price" name="unit_price[' + counter + ']" onkeypress="return isNumberKey(event)"></td>';
        cols += '<td class="line_vat">£0.00</td>';
        cols += '<td class="line_total">£0.00</td>';
        cols += '<td><input type="button" class="ibtnDel btn btn-md btn-danger"  value="Delete"></td>';
        newRow.append(cols);
        $("table.table-striped").append(newRow);
        counter++;
    });

    $("table.table-striped").on("click", ".ibtnDel", function (event)
    {
        $(this).closest("tr").remove();
        /* -------------------------------------- */
        /* commenting decreasing "counter" for better functionality after deleting rows */
        /* -------------------------------------- */
        // counter -= 1
    });

    /* -------------------------------------- */
    /* converting line_qty and line_price to arrays to not have conflict between rows */
    /* -------------------------------------- */
    var line_qty = [0];
    var line_price = [0];

    /* -------------------------------------- */
    /* change "blur" event call according to other stack-overflow question for handling dynamically created DOM */
    /* -------------------------------------- */
    $("#quote_table").on("blur", ".line-quantity, .line-price", function () {
        console.log($(this));

        var line_subtotal = '0.00';
        var vat = '0.00';
        var line_total = '0.00';
        /* -------------------------------------- */
        /* using name "attr" to understand which index of "line_qty" and "line_price" arrays must be changed */
        /* -------------------------------------- */
        let indexBrackets = $(this).attr("name").indexOf("[") + 1;
        let rowNum = $(this).attr("name").substr(indexBrackets, 1)

        /* -------------------------------------- */
        /* using "jQuery hasClass()" method to find which input in each row has blur event */
        /* -------------------------------------- */
        if ($(this).hasClass("line-quantity")) {
                line_qty[rowNum-1] = $(this).val();
        }

        console.log(line_qty);

        if ($(this).hasClass("line-price")) {
            line_price[rowNum-1] = $(this).val();
        }

        console.log(line_price);


        // calculate total line price
        /* -------------------------------------- */
        /* using arrays to calculate final values */
        /* -------------------------------------- */
        line_subtotal = line_price[rowNum-1]*line_qty[rowNum-1];

        // Calculate VAT
        vat = line_subtotal*1.2-line_subtotal;

        // Line total
        line_total = line_subtotal+vat;
        console.log(line_total);

        // Update view with totals
        $(this).closest('tr').find('td.line_vat').html(vat.toLocaleString('en-US', {style: 'currency',currency: 'GBP'}));
        $(this).closest('tr').find('td.line_total').html(line_total.toLocaleString('en-US', {style: 'currency',currency: 'GBP'}));

        // Calculate totals at end of table
    });

});

function isNumberKey(evt){
    var charCode = (evt.which) ? evt.which : evt.keyCode;
    if (charCode != 46 && charCode > 31 && (charCode < 48 || charCode > 57))
        return false;
    return true;
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<form method="post" action="{{ url('cp/quotes/create') }}">
    {{ csrf_field() }}
    <div class="form-group">
        @if($contacts->isEmpty())
        <a href="/cp/contacts/create">Create first contact</a>
        @else
        <label for="contact">Contact</label>
        <!-- Get the selected contact and fetch properties -->
        <select class="form-control selectpicker show-tick" data-live-search="true" id="contact" name="contact" data-live-search-placeholder="Find contact...">
            <option value="">Select a contact...</option>
            @foreach($contacts as $contact)
            <option data-tokens="{{ $contact->id }}" value="{{ $contact->id }}">{{ $contact->contact_name }} ({{ $contact->first_name }} {{ $contact->last_name }})</option>
            @endforeach
        </select>
        <!-- List contact properties -->
        <div class="btn-group-vertical property_list" role="group" aria-label="Property" id="property_list"></div>
        @endif
    </div>
    <div class="form-group">
        <label class="control-label" for="raised_date">
            Date
        </label>
        <div class="input-group">
            <div class="input-group-addon">
                <i class="fa fa-calendar"></i>
            </div>
            <input class="form-control" id="raised_date" name="raised_date" type="text"/>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label" for="raised_date">
            Expiry Date
        </label>
        <div class="input-group">
            <div class="input-group-addon">
                <i class="fa fa-calendar"></i>
            </div>
            <input class="form-control" id="expiry_date" name="expiry_date" type="text"/>
        </div>
    </div>
    <table class="table table-striped" id="quote_table">
        <thead>
        <tr>
            <th scope="col">Item #</th>
            <th scope="col">Description</th>
            <th scope="col">Quantity</th>
            <th scope="col">Unit Price</th>
            <th scope="col">VAT</th>
            <th scope="col">Amount</th>
            <th scope="col"></th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td>1</td>
            <td><textarea class="form-control" name="description[]"></textarea></td>
            <!-- ///////////////////////////// -->
            <!-- change "quantity[]" to quantity[1] and "unit_price[]" to unit_price[1] to use it in js codes -->
            <!-- ///////////////////////////// -->
            <td><input class="form-control line-quantity" name="quantity[1]" type="text" onkeypress="return isNumberKey(event)"></td>
            <td><input class="form-control line-price" name="unit_price[1]" type="text" onkeypress="return isNumberKey(event)"></td>
            <td class="line_vat">£0.00</td>
            <td class="line_total">£0.00</td>
            <td></td>
        </tr>
        </tbody>
        <tfoot>
        <tr>
            <td colspan="5" class="text-end">Subtotal</td>
            <td colspan="2" class="subtotal">£0.00</td>
        </tr>
        <tr>
            <td colspan="5" class="text-end h3">Total</td>
            <td colspan="2" class="total h3">£0.00</td>
        </tr>
        </tfoot>
    </table>
    <a class="btn btn-warning addRow" href="#" id="addRow">Add Line +</a>
    <div class="form-group">
        <br /><button type="submit" class="btn btn-success">Save Quote</button>
    </div>
</form>


</body>
</html>

I tried to comment changes in codes, but the main changes are using new "blur" calling that mentioned in that question and also separating the event handling for each row by using arrays instead of numbers.

hamid-davodi
  • 1,602
  • 2
  • 12
  • 26
  • thanks, that worked great! Any suggestions to go one step further and add all the line totals together for a grand total at the bottom? – V4n1ll4 Mar 03 '22 at 18:43