DJ MHA
DJ MHA

Reputation: 598

Error in updating field with dynamically generated row

I have inventory form to submit inventories to database. I'm facing an issue in updating unit cost and total cost of dynamically generated rows. As you can see in snapshots below. The name of products are fetching via autocomplete jQuery.

enter image description here

HTML CODE

<table class="table table-bordered table-hover">
    <thead>
        <tr>
            <th>#</th>
            <th>Product Name/Code</th>
            <th>Quantity</th>
            <th>Unit Cost</th>
            <th>Total Cost</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody id="p_scents">
        <tr>
            <td>1</td>
            <td><input id="product_id" type="text" name="product_id[]" hidden><input id="product_code" type="text" name="product_code[]" hidden><input class="product_name form-control" id="product_name" type="text" placeholder="Type product name/code here" name="products[]" required></td>
            <td><input class="quantity form-control" id="quantity" type="text" placeholder="Quantity to Buy" name="quantity[]" required /></td>
            <td><div class="input-group"><span class="input-group-addon">$</span><input class="cost form-control" id="cost" placeholder="Unit Cost" type="text" readonly /></div></td>
            <td><div class="input-group"><span class="input-group-addon">$</span><input class="total form-control" id="total" placeholder="Total" type="text" readonly /></div></td>
            <td><button class="btn btn-default" type="button" id="addScnt"><i class="fa fa-plus "></i> Item</button></td>
        </tr>
    </tbody>
</table>

jQuery CODE

<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script type='text/javascript'>

jQuery(document).ready(function(){
    var scntDiv = $('#p_scents');
    var i = $('#p_scents tr').size() + 1;

    $('#addScnt').click(function() {
        scntDiv.append('<tr>'+
        '<td>'+i+'</td>'+
        '<td><input id="product_id" type="text" name="product_id[]" hidden><input id="product_code" type="text" name="product_code[]" hidden><input class="product_name form-control" id="product_name" type="text" placeholder="Type product name/code here" name="products[]" required></td>'+
        '<td><input class="quantity form-control" id="quantity" type="text" placeholder="Quantity to Buy" name="quantity[]" required /></td>'+
        '<td><div class="input-group"><span class="input-group-addon">$</span><input class="cost form-control" id="cost" placeholder="Unit Cost" type="text" readonly /></div></td>'+
        '<td><div class="input-group"><span class="input-group-addon">$</span><input class="total form-control" id="total" placeholder="Total" type="text" readonly /></div></td>'+
        '<td><a id="remScnt" class="btn btn-danger btn-xs"><span title="Delete" class="glyphicon glyphicon-remove"></span></a></td>'+
        '</tr>');
        i++;
        //return false;
        $('.product_name').autocomplete({
            source:'http://localhost/Multi-Channel_Shipping/inc/auto_product.php',
            minLength:2,
            select:function(evt, ui)
            {
                // when a product is selected, populate related fields in this form
                this.form.cost.value = ui.item.cost;
                this.form.product_id.value = ui.item.product_id;
                this.form.product_code.value = ui.item.product_code;
            }
        });

        $('.quantity').keyup(function() {  
            updateTotal();
        });
        $('.cost').keyup(function() {  
            updateTotal();
        });

        var updateTotal = function () {
          var input1 = parseFloat($('.quantity').val());
          var input2 = parseFloat($('.cost').val());
          if (isNaN(input1) || isNaN(input2)) {
              if(!input2){
                    $('.total').val($('.quantity').val());
              }

              if(!input1){
                    $('.total').val($('.cost').val());
              }

          } else {          
                $('.total').val(input1 * input2);
          }
        };

        var output_total = $('.total');
        var total = input1 + input2;
        output_total.val(total);

    });

    //Remove button
    $(document).on('click', '#remScnt', function() {
        if (i > 2) {
            $(this).closest('tr').remove();
            i--;
        }
        return false;
    });

    $('.product_name').autocomplete({
        source:'http://localhost/Multi-Channel_Shipping/inc/auto_product.php',
        minLength:2,
        select:function(evt, ui)
        {
            // when a zipcode is selected, populate related fields in this form
            this.form.cost.value = ui.item.cost;
            this.form.product_id.value = ui.item.product_id;
            this.form.product_code.value = ui.item.product_code;
        }
    });


    $('.quantity').keyup(function() {  
        updateTotal();
    });
    $('.cost').keyup(function() {  
        updateTotal();
    });

    var updateTotal = function () {
      var input1 = parseFloat($('.quantity').val());
      var input2 = parseFloat($('.cost').val());
      if (isNaN(input1) || isNaN(input2)) {
          if(!input2){
                $('.total').val($('.quantity').val());
          }

          if(!input1){
                $('.total').val($('.cost').val());
          }

      } else {          
            $('.total').val(input1 * input2);
      }
    };

    var output_total = $('.total');
    var total = input1 + input2;
    output_total.val(total);

});
</script>

AUTO_PRODUCT.PHP CODE

<?php

class DB
{
    const DATABASE = 'multi-channel_shipping';
    const HOST = 'localhost';
    const USERNAME = 'root';
    const PASSWORD = '';

    static private $pdo;

    static public function singleton()
    {
        if (!is_object(self::$pdo))
        {
            self::$pdo = new PDO('mysql:dbname=' . self::DATABASE . ';host=' . self::HOST, 
                                    self::USERNAME, 
                                    self::PASSWORD);
        }
        return self::$pdo;
    }

    private function __construct()
    {

    }

    public function __clone()
    {
        throw new Exception('You may not clone the DB instance');
    }
}

if (!isset($_REQUEST['term']))
{
    die('([])');
}

$st = DB::singleton()
        ->prepare(
            'SELECT * ' .
            'FROM products ' .
            'WHERE (name LIKE :name) OR (code LIKE :name) ' .
            'ORDER BY name ASC ' .
            'LIMIT 0,10');

$searchProduct = '%'.$_REQUEST['term'].'%';
$st->bindParam(':name', $searchProduct, PDO::PARAM_STR);

$data = array();
if ($st->execute())
{
    while ($row = $st->fetch(PDO::FETCH_OBJ))
    {
        $data[] = array(
            'value' => $row->code." - ".$row->name,
            'cost' => $row->cost,
            'product_id' => $row->id,
            'product_code' => $row->code
        );
    }
}
echo json_encode($data);
flush(); 
?>

MySQL Data

--
-- Table structure for table `products`
--

CREATE TABLE IF NOT EXISTS `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(100) NOT NULL,
  `name` varchar(255) NOT NULL,
  `unit` varchar(50) DEFAULT NULL,
  `cost` decimal(25,2) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `code` (`code`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;

--
-- Dumping data for table `products`
--

INSERT INTO `products` (`id`, `code`, `name`, `unit`, `cost`) VALUES
(1, '4815162342', 'BAZIC 12 Dil Dil Pak', 'Packet', '0.10'),
(2, '23', 'Razer', 'Piece', '0.03');

I also need to put shipping cost input field and show grand total of invoice at bottom of table.

Upvotes: 1

Views: 1367

Answers (2)

iCollect.it Ltd
iCollect.it Ltd

Reputation: 93571

There a multiple issues with the page & code, so I will try to cover what I can. @Barmar also spotted additional issues so will try to cover everything and suggest some improvements.

JSFiddle: http://jsfiddle.net/TrueBlueAussie/vx15mr4n/29/

Templating:

Rather than use text strings in code, it is easier to maintain HTML as HTML. The example I have provided uses a dummy script block (of type="text/template", which will be ignored by all browsers) but you can access the HTML content with $('#template').html().

Duplicate ID are invalid

You can't have duplicate IDs in a page. That is invalid HTML and jQuery will only see the first match. Use classes on the added elements instead and match on those.

so use:

<a class="remScnt" 

and

$(document).on('click', '.remScnt', function() 

Note: you will need to sort out any other duplicate IDs too (like product_id and quantity and cost and total). Your code already uses classes for those, so just move/delete the id attributes.

e.g. use classes for everything:

    scntDiv.append('<tr>'+
    '<td>'+i+'</td>'+
    '<td><input class="product_id" type="text" name="product_id[]" hidden><input id="product_code" type="text" name="product_code[]" hidden><input class="product_name form-control" type="text" placeholder="Type product name/code here" name="products[]" required></td>'+
    '<td><input class="quantity form-control" type="text" placeholder="Quantity to Buy" name="quantity[]" required /></td>'+
    '<td><div class="input-group"><span class="input-group-addon">$</span><input class="cost form-control" placeholder="Unit Cost" type="text" readonly /></div></td>'+
    '<td><div class="input-group"><span class="input-group-addon">$</span><input class="total form-control" placeholder="Total" type="text" readonly /></div></td>'+
    '<td><a class="remScnt btn btn-danger btn-xs"><span title="Delete" class="glyphicon glyphicon-remove"></span></a></td>'+
    '</tr>');

You are using a delegated event for one handler but not the others. You also need to add them for keyup (which can be combined as the code is the same):

$('#p_scents').on('keyup', '.quantity .cost', function() {  
    updateTotal();
});

IMPORTANT: Your code here is not matching a specific row. Also use @Barmar's fix like this to pass the current row:

$('#p_scents').on('keyup', '.quantity .cost', function() {  
    updateTotal($(this).closest('tr'));
});

Update: As Regent mentions below, you should not use document but use #p_scents for your delegated event handler:

$('#p_scents').on('click', '.remScnt', function() 

A delegated event should be attached to the closest non-changing ancestor (if one is convenient/available). This will give a very small speed increase as it stops lower in the DOM.

I also cleaned up the event handler doing the calculations which now using temp vars, for elements relative to the row, and looks like:

// Update the row total of a specific row
var updateTotal = function ($row) {
    // Get the specific inputs
    var $quantity = $('.quantity', $row);
    var $cost = $('.cost', $row);
    var $total = $('.total', $row);
    var input1 = parseFloat($quantity.val());
    var input2 = parseFloat($cost.val());
    if (isNaN(input1) || isNaN(input2)) {
        if (!input2) {
            $total.val($quantity.val());
        }

        if (!input1) {
            $total.val($cost.val());
        }

    } else {
        $total.val(input1 * input2);
    }
    var total = input1 * input2;
    $total.val(total);
};

Note: Without the missing data, I cannot easily test the code, but you should get the idea.

Grand total

To update the grand total, you need to iterate all .total fields and add them to the shipping cost:

   var updateGrandTotal = function()
   {
       // Now update the grand total
       var grandTotal = 0;
       $('.total').each(function () {
           grandTotal += parseFloat($(this).val());
       });
       var shipping = parseFloat($('.shippingcost').val());
       $('.grandtotal').val(grandTotal + shipping);
   }

As you will want to update the grand total when the shipping changes, I refactored it out so it could also be called from a keyup on the shipping:

   $('.shippingcost').keyup(function(){
       updateGrandTotal();
   });

The other issue is the autocomplete (which I could not test without a real data feed):

Basically get the select event to refer to the current field's row and find the appropriate fields to update:

JSFiddle: http://jsfiddle.net/TrueBlueAussie/vx15mr4n/23/

   select: function (evt, ui) {
       // when a product is selected, populate related fields in this form
       var $tr = $(this).closest("tr");
       $(".cost",$tr).val(ui.item.cost);
       $(".product_id", $tr).val(ui.item.product_id);
       $(".product_code", $tr).val(ui.item.product_code);
   }

Upvotes: 3

Barmar
Barmar

Reputation: 781058

When updateTotal() uses $('.quantity').val() it gets the value of the first field with that class, not the one in the row that the user was typing in. You need to pass the row to the function. Also, since the elements are added dynamically, you need to use delegation for the event bindings.

$('#p_scents').on('keyup', '.quantity, .cost', function() {  
    updateTotal($(this).closest('tr'));
});

var updateTotal = function (row) {
    var input1 = parseFloat($('.quantity', row).val());
    var input2 = parseFloat($('.cost', row).val());
    if (isNaN(input1) || isNaN(input2)) {
        if(!input2){
            $('.total', row).val(input1);
        }

        if(!input1){
            $('.total', row).val($(input2);
                                }

        } else {          
            $('.total', row).val(input1 * input2);
        }
    }
    var output_total = $('.total', row);
    var total = input1 + input2;
    output_total.val(total);

};

Upvotes: 0

Related Questions