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 and quantity 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.
Any suggestions guys ?!?
You want to take the wrapping element
<div>
<input />
<input />
</div>
$('div').on('change', 'input', function(){
// your magic here
});
This will work on the two who are there now, and new elements as well. Why? Simply put:
You bind the events to elements that exists. You make a new one, change it, but never bound the event to the new elements. The event bubbles up the tree, nothing to catch it.
My code doest bind to the elements itself, your telling it to listen to changes in it on input. New elements come in, you change them, nothing happends, so it bubbles up the tree.
And this is the big difference: this time we told the existing element to do something now.
Related
I have this piece of JavaScript that will sum up a totalprice based on radio/checkbox selection
JQUERY
var price = 0;
$('.price-input').click(function () {
if ($(this).is(":checked")) {
price += parseInt($(this).attr("data-price"), 10);
$('.total-price').text(price);
$('.total-price-input').val(price);
} else {
price -= parseInt($(this).attr("data-price"), 10);
$('.total-price').text(price);
$('.total-price-input').val(price);
}
});
The HTML is a collection of 3 radiobuttons and 3 checkboxes, where the radiobutton is one price and the checkboxes can sum up 3 prices with the radiobutton price to give a total price.
<input class="price-input" data-price="2000" id="ChosenLane" name="ChosenLane" type="radio" value="Konference - Spor 1: Lean">
<input class="price-input" data-price="1300" id="ChosenLane" name="ChosenLane" type="radio" value="Konference - Spor 2: Innovation">
<input class="price-input" data-price="1600" id="ChosenLane" name="ChosenLane" type="radio" value="Kage">
<input type="checkbox" name="lanes" value="Aften buffet" data-price="1000" class="price-input">
<input type="checkbox" name="lanes" value="fthrctfyh" data-price="5456" class="price-input">
<input type="checkbox" name="lanes" value="dfhxfhg" data-price="54545" class="price-input">
All in all this works - if I select a radiobutton it will add the radiobutton price to the price, but if I click a radiobutton again it will add the price again to the totalprice. I want the radiobuttons, not to add if one is selected, but instead just use its price.
So if I check a radiobutton it will at 2000 to the price and if I click another radiobutton, it will subtract the 2000 and add 1300 instead.
Please check the following solution:
JQUERY
$(function () {
$('.price-input').change(function () {
var price = 0;
$('.price-input').each(function(){
if ($(this).is(":checked")) {
price += parseInt($(this).attr("data-price"), 10);
}
})
$(".totalPrice").text(price);
});
})
Fiddle: http://jsfiddle.net/kkqqfaqv/
Change your code to change event from click event. As click event is called every time user clicks the radio box, doesn't matter whether selected or not. Change event will be triggered if the state of checkbox changes
I am not entirely clear about your relation betweeen price and total-price-input.
As a general rule for much higher robustness: Don't rely on clean unsetting-setting, i.e. that substracting X and adding Y always match up one on one.
Rather:
always start your callback function with price=0
check which input buttons and radio button are set
add up accordingly
This has also the benefit, that you can trigger such a clean-from-start calback after page DOM load: I.e. if user goes browser-forward, then back, inputs and radio buttons keep being checked (in any modern browser) ➠ having the correct value for that resurrected choice (rather than 0 and a “refresh problem” is good. ➠ Thus put it in a named distinct function, to call upon $.ready() and upon .change().
Also, I'd use .change() rather than .click(): Input can possibly change state by other means than click (like keyboard navigation?), and change is what you are looking for.
I have a table with a button that adds a row to the bottom by cloning the last row. Each row contains a <select> with a different name (0-9), all <select> objects have the .variable_priority class. When pressing the button, the row gets copied successfully and I manage to change the name correctly (10, 11, 12, etc.), and the row gets the same class since it is a perfect copy.
However, when I execute the following function, I encounter problems with cloned table rows:
$('.variable_priority').change(function() {
var selections = new Array();
$(".variable_priority").each(function() {
selections.push($(this).find('option:selected').val());
});
// Iterating on Options of Select
$('.variable_priority').children('option').each(function() {
$(this).removeAttr('disabled');
if($.inArray($(this).val(), selections) !== -1){
if($(this).val() != 0){
if($(this).is(':selected')){
// nothing
} else {
$(this).attr('disabled', true);
}
}
}
});
});
The function should run each time I change the selected option of a <select>, though it only runs with the 10 original ones. This function is not executed when I change the option in a cloned row. However, if I change the value in one of the originals, the function runs and it also adds the values from the cloned rows to the selections array.
So for some reason, the code recognizes that the clones have the same class, but it doesn't run when changing their values.
EDIT: short description of the code above; if I change the selected value of a <select>,the function will add all selected options to an array and disable every selected option in the selects, except in the box where the value itself is selected (if I select '2' in the first box, '2' gets disabled in all other boxes).
Because your select element is dynamic (It is added to the DOM after the initial page load, or to be specific; after the initial event handler attachment) you will need to delegate your change event handler:
$(document).on('change', '.variable_priority', function() {
});
Rather than use document though, you'll want to attach the event handler to the closest static element to .variable_priority.
JSFiddle
I think the code isn't executed on the new clones.
This script is loaded on start. It has to do it over for each new children.. Maybe after adding a new, run this script specific for that new one.
I am trying to calculate discounted price which I am able to do without anyproblem for a single item. I also have 'add more' button to add many item as I can. So here, I started facing problem in calculating the discounted price for these dynamically added input field.
My default discount calculation script for single item is
$('#discount_1').change(function(){
var quantity=$('#qty_1').val();
var percent=$('#discount_1').val();
var price=$('#price_1').val();
var tprice = price * quantity
var discountpercent=percent / 100;
var discountprice=(tprice * discountpercent );
$('#total_1').val(tprice - discountprice);
});
I tried changing it to the following
$(":input[id^='discount_']").change(function(){
var quantity=$(":input[id^='qty_']").val();
var percent=$(":input[id^='discount_']").val();
var price=$(":input[id^='price_']").val();
var tprice = price * quantity
var discountpercent=percent / 100;
var discountprice=(tprice * discountpercent );
$(":input[id^='total_']").val(tprice - discountprice);
});
to calculate discount for all the item set having id attribute that starts with qty_, which actually does not seem to work properly.
here is my jsFiddle
I've modified this to remove all ids. This will clean up a lot of extra crap. Here is a fiddle: http://jsfiddle.net/B5T6R/1/
The solution involved event delegation. The problem with $(":input[id^='discount_']").on('change' is that the future trs don't exist yet, so there is nothing to bind to!
The solution is:
$("#InputsWrapper").on('change', '.discount'
Which will listen for all future changes on the InputsWrapper table as a whole, not just to the discount elements.
The problem is that the selectors you are using will always select the first input that matches the selector.
So, (":input[id^='qty_']") will always match the input on the first row.
I suggest:
Rebind the "change" event evertime AddButton is clicked. This will
require .unbind() as well.
Add class "discount" to all discount inputs.
Change the selectors for quantity, price, etc to be relative to the input that was changed.
IE:
$('.discount').unbind().change(function(){
var $parentRow = $(this).parent().parent();
var quantity=$(":input[id^='qty_']", $parentRow).val();
});
It's not a great idea to use all these id's: #qty_1, #qty_2, etc. Instead give all your inputs the same class names to hook into, for example:
<input class='discount' type='text' name='discount'/>
<input class='quantity' type='text' name='quantity'/>
Then use good ole Jquery to traverse the DOM and fetch the relevant data. In this case you have to climb to the closest td and then back down to get the .quantity class, as so:
$(".discount").change(function(){
var quantity = $(this).closest('td').find('.quantity').val();
});
Hope this helps.
You are running into a couple of problems.
First, only the ":input[id^='discount_']" that exist on your page when the DOM is initialized have this change handler added to them - all new rows added via the the Add More Field link will not have this handler bound. You can get around this by attaching the handler to the container all of your fields are in via .on, so that every change event fired within that container will be checked against the selector specified. I've forked your fiddle to demonstrate: http://jsfiddle.net/gLz9B/1/
$('#InputsWrapper').on("change", ":input[id^='discount_']", function(){
...
});
The second issue is that the qty_, total_, price_, and discount_ selectors you are using will return arrays, rather than being limited to the specific row where the change is occurring. In my fiddle fork I did a string replace to get the unique number attached to the id of the element, and then build the ids of all of the other inputs rather than using =^ to select them. This is not the only way to limit your scope, but it works given your sample code.
var id = this.id.replace('discount_','');
var quantity=$("#qty_" + id).val();
I have a problem with my JQuery.
My code works like this,
There is a table of names with input boxes at the bottom of the table to add a new name and role, when both of the input boxes are filled in, the code adds the values of the input boxes to a new table row.
My problem is, when the content is added, I want to add another row of input boxes so more people can be added, however... The function that runs when the input boxes have lost focus/blurred is bound to my original input elements and when the next row of inputs is added, I am basically trying to bind the function i'm currently running to the element... Is this possible or how else should I tackle this..
Code:
$('.add_new_castcrew').bind("blur",function(){
castCrewBlur(this);
});
function castCrewBlur(element){
if(castChangeLength == 2){
$('#newCast').removeAttr('id');
console.log("HAHAHAHA");
$('#cast_table').append("<tr id=\"newCast\"><td><input type=\"text\" value=\"Cast Member Name\" class=\"add_new_castcrew\" id=\"new_cast_name\"></td><td><input type=\"text\" value=\"Cast Member Role\" class=\"add_new_castcrew\" id=\"new_cast_role\"></td></tr>");
castChangeLength = 0;
$('#newCast.contentAdded ').each(function(){$(element).removeClass("contentAdded"); console.log("Removed"); });
$('.add_new_castcrew').bind("click",function(){
newCastCrewClick(this);
});
$('.add_new_castcrew').bind("click",function(){
castCrewBlur(this);
});
"it is simply the part where I am binding a blur to the newly appended
input box"
It seems like you're actually binding to the .click()...
$('.add_new_castcrew').bind("click",function(){
castCrewBlur(this);
});
I have a series of textboxes with the following IDs:
118|1/9/2011
118|1/10/2011
118|1/11/2011
118|1/12/2011
118|1/13/2011
118|1/14/2011
118|1/15/2011
118|Total
Using jQuery or just javascript, I need to sum each textbox and assign the value to the total textbox. This needs to happen when a user tabs off or clicks off the textbox. It also needs to be generic enough to handle textboxes with similar IDs such as:
119|1/9/2011
119|1/10/2011
119|1/11/2011
119|1/12/2011
119|1/13/2011
119|1/14/2011
119|1/15/2011
119|Total
The format stays the same. Only the first numbers to the left of the | will change.
Any guidance would be greatly appreciated.
EDIT: I updated my answer to reflect the actual example HTML that Mike provided, which you can view in this fiddle. That link also contains the working javascript from below.
If you have a specific selector for the inputs you want to sum (like a class name), as well as one for the total, you should be able to do this (here's a fiddle link with this javascript in action: http://jsfiddle.net/avidal/zfjmD/):
$('input.sum').change(function() {
var sum = 0;
// we want to sum up the values of all input.sum elements that are in the same tr
// as this one
$(this).closest('tr').find('input.sum').each(function(i) {
var val = parseInt($(this).val(), 10);
/*
change the above line to:
var val = parseFloat($(this).val());
if the inputs will be floats
*/
if (isNaN(val) || val === undefined) {
return;
}
sum += val;
});
$(this).closest('tr').find('input.total').val(sum);
});
The important things to note are the .closest() function, and the .find() function.
First, we bind to the change event for all inputs with a class of sum. When one of those elements is changed (the value changes, then the user clicks/tabs out), we look for the closest tr element, then find the list of inputs with a class of sum that are children of that tr. For each of those inputs, we parse the value and accumulate the sum. After we've iterated over each of those inputs, we finally find the input with a class of total that's a descendant of the closest tr, and set the val of that input to the accumulated sum
Perfect!
Great question and great answer! This works perfectly! To answer Mike, I changed 'tr' to 'table' in the jQuery and it totals all the sums in a table across several tr's and td's. As long as the inputs have the class="sum" they will be totaled.