Background
I'm not the hottest jQuery guy out there by a very long shot and I'm trying to strip out the repeated work in the below code. Whilst the performance overhead is probably minimal and negligible - this is more a case of not wanting to write crappy code that does the same thing several times.
Basically I have a simple invoice form, that a user can add multiple items too.
The form initially has 4 inputs: Item Name, Item Price, Item Quantity and Total
- The total is calculated whenever the Price or Quantity field fires a change event
Problem - Partially resolved (See Update)
The user can add an additional row of inputs for a second (or third, fourth, fifth, etc...) item
- The existing javascript which attaches the event handler to the
price
andquantity
fields has already run so will not attach listeners to the newly added row of inputs.
Solution ?
Currently I've hashed out something horrible whereby after adding the new row of inputs I re-attach an event listener to all input fields.
That's cool I guess, if you take no pride in the quality of your work, but if the invoice is 20 items do I really need to on adding the 20th item row loop over the 19 rows that already have listeners attached, attach them again and then attach listeners to the new row. I would hope not.
I've already managed to target the newly added row of inputs to wipe the values from the cloned inputs - so I'm thinking just target the new input fields and attach listeners - but I'm getting in a right two and eight because ideally I'd like to do this like so
- Clone the input row
- Clear the values
- Attach listeners
- Add to the DOM
What I'm currently doing which feels grotesque is
- Clone the row
- Add the row to the DOM
- Select the newly added row and wipe the values
- Select the newly added Quantity field and attach a listener
- Select the newly added Price field and attach a listener
- Select the newly added Total field and attach a listener (to update the invoice total)
Code below, for you to laugh at and then hopefully take pity on me and provide a more succinct solution or at least a suggestion as to how to go about writing my own better version.
/** Add additional item lines */
$('#add-item').click(function(e){
e.preventDefault();
/** clone first line and insert it */
$('.input-row:first').clone().insertAfter('.input-row:last');
/** clear the newly inserted inputs of values */
$(':input', '.input-row:last').val("");
/** ensure all item price and qty inputs have events attached to their change value */
$('input[name="item_qty[]"],input[name="item_price[]"]').on("change",function () {
var $container = $(this).closest('.form-group');
qty = Number($('input[name="item_qty[]"]',$container).val())||0,
price = Number($('input[name="item_price[]"]',$container).val())||0;
$('input[name="item_total[]"]',$container).val(qty * price);
$('input[name="item_total[]"]',$container).change();
});
/** Sum inputs for invoice total */
$('input[name="item_total[]"').change(function() {
var total = 0;
$.each($("[name='item_total[]']"), function(index, value) {
total += parseFloat($(this).val());
});
$("#total").val(total);
});
});
Update
So by utilising event delegation, events propagate (or bubble) up the dom - thanks guys! I've got the invoice total being recalculated any time one of the inputs within the new parent div change
<div id="invoice-items">
<input name /> <input quantity /> <input price /> <input total />
<input name /> <input quantity /> <input price /> <input total />
<input name /> <input quantity /> <input price /> <input total />
...
</div>
/** if any input rows change update the invoice total */
$('#invoice-items').on('change', 'input', function(event){
var total = 0;
$.each($("[name='item_total[]']"), function(index, value) {
total += parseFloat($(this).val());
});
$("#total").val(total);
});
Problem I'm left with...
I'm still stuck on how I go about updating <input total />
to reflect the changes to that particular line. I'm guessing somewhere in my new jQuery snippet I need to determine which field changed and update the total on the same row ?
This is how I'm currently attaching the change listeners to the first / existing row of input to populate the line total
/** calculate item total */
$('input[name="item_qty[]"],input[name="item_price[]"]').on("change", function () {
var $container = $(this).closest('.form-group');
qty = Number($('input[name="item_qty[]"]',$container).val())||0,
price = Number($('input[name="item_price[]"]',$container).val())||0;
$('input[name="item_total[]"]',$container).val(qty * price);
$('input[name="item_total[]"]',$container).change();
});
I guess what I still need is some means to run this code after a line has been added, or following the cleaner event delegation route - some way to target just the item_total[]
for the row in which the change event happens ? Maybe I can capture the specific index of the element on which the change event is fired - and update only the item_total[]
at that index ?
Just thinking out loud here, I guess if I capture the event and loop through all of the inputs til I find that element which matches the element the event was fired from I could then grab the next form input with the name invoice_total[]
and update it ? - let's go check.
Update
So I can capture the event - happy days :)
event.currentTarget.attributes.name.nodeValue == 'item_qty[]'
So I still don't know which of the item_qty[]
elements I've updated and therefore I don't know which item_total[]
element to update.