Ask a Jedi: Dynamically updating line items on a form

This post is more than 2 years old.

Sid had the following problem. He has a form with a dynamic number of line items. Each line item has 3 fields. A product quantity, a price per product, and a total for the line item. He wanted to know if it was possible to automatically update the total as you entered prices and quantities, and to also have a grand total. This is solvable via JavaScript of course, with a bit of complexity involved to handle the dynamic number of fields. Here is how I solved it - with both my own JavaScript and a jQuery solution.

First let's begin by creating our form. I've hard coded the number of line items.

<cfset numItems = 5>

<form id="lineitems"> <table> <tr> <th>Product Qty</th> <th>Price Per Unit</th> <th>Total</th> </tr>

<cfloop index="x" from="1" to="#numItems#"> <cfoutput> <tr> <td><input type="text" name="qty_#x#" id="qty_#x#" onChange="updateData(#x#)"></td> <td><input type="text" name="ppu_#x#" id="ppu_#x#" onChange="updateData(#x#)"></td> <td><input type="text" name="total_#x#" id="total_#x#"></td> </tr> </cfoutput> </cfloop>

&lt;tr&gt;&lt;td colspan="3" bgcolor="yellow"&gt;&lt;B&gt;TOTALS:&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;
	&lt;td&gt;&lt;input type="text" name="total_qty" id="total_qty"&gt;&lt;/td&gt;
	&lt;td&gt;&nbsp;&lt;/td&gt;
	&lt;td&gt;&lt;input type="text" name="total_price" id="total_price"&gt;&lt;/td&gt;
&lt;/tr&gt; 

</table> </form>

Notice that for each line item, I've used a dynamically named ID based on the current row. Also note I use an event handler to pass the row number to an updateData function. I check the change event for both quantity and the price per unit field. (My examples will assume that folks don't mess with the totals.)

Lastly I added a row for the grand totals. It didn't make sense to me to have a grand total of the price per unit column, so I just use a grand total for quantity and the price.

Ok, now let's look at the JavaScript:

<script> function updateData(x) { //x is the index var qty = document.getElementById('qty_'+x).value; var ppu = document.getElementById('ppu_'+x).value; var liTotal = document.getElementById('total_'+x); //if we have numbers for both, do math, else, nuke result if(!isNaN(qty) && !isNaN(ppu)) liTotal.value = qty*ppu; else liTotal.value = '';
//now update grand total
var i = 1;
var totalQty = 0;
var totalPrice = 0;
while(thisQty = document.getElementById('qty_'+i)) {
	qty = document.getElementById('qty_'+i).value;
	liTotal = document.getElementById('total_'+i).value;
	if(!isNaN(qty)) totalQty += Number(qty);
	if(!isNaN(liTotal)) totalPrice += Number(liTotal);
	i++;
}
document.getElementById('total_qty').value = totalQty;
document.getElementById('total_price').value = totalPrice;

} </script>

So what am I doing here? I first get both the quantity and price per unit fields for the current row. If both are numbers, I do simple math to get the total.

Now look at the second part. This is going to loop over all my rows. I used a while loop, but since the total number of rows was generated server side, I could have output that as a hard coded JavaScript variable as well. For each row I get the quantity and the total. If they are numbers, I add them each to a variable. My final step is to update the grand total fields.

Ok, easy enough, right? To get me some more practice in jQuery, I decided to rewrite the template using jQuery code. Please remember I'm new at jQuery so this may not be the best way to do things.

The first thing I wanted to do was to get rid of the event handlers. I knew that jQuery had a way to select all items of a certain type. I also knew that jQuery could add event handlers to things. So my first job was to combine that into one action. I began with the basic "run this on document ready" wrapper:

$(document).ready(function() { //insert Skynet here... });

Now for the hard part. If you remember the previous example, I had change events on the quantity and ppu fields. I needed a way to select all those fields. My knowledge of jQuery selectors was a bit slim - but I found that I could select all input fields based on an ID property. What I couldn't figure out was how to do "ID begins with qty_ or ID begins with ppu+". So I cheated. I modified my CFML to use dyn_ in front of all my IDs for the line items:

<td><input type="text" name="qty_#x#" id="dyn_qty_#x#"></td> <td><input type="text" name="ppu_#x#" id="dyn_ppu_#x#"></td>

Once I had that, I could then write my selector:

$("input[@id^='dyn_']").bind("change",function(e) { //smart logic here });

Note the @id^'dyn_'. This is what means "all IDs that begin with dyn_. Since it's an attribute of input, it will only match inside input form fields. The .bind("change" portion says to bind the function I'm about to define to the change event.

Ok, now I have a new problem. I'm adding an event handler to both qty and ppu fields, but how do I know which one was changed? The event handler is passed "this", which represents the item that changes. That has an ID. I can use regex then to get just the number:

var myid = this.id; //get the Index var index = myid.replace(/dyn_(qty|ppu)_/,"");

Now I'm back, kinda, to what I had before, a row number. The rest of the code is pretty similar to the previous edition, just a bit simpler:

var qty = $("#dyn_qty_"+index).val(); var ppu = $('#dyn_ppu_'+index).val(); var liTotal = $('#total_'+index); //if we have numbers for both, do math, else, nuke result if(!isNaN(qty) && !isNaN(ppu)) liTotal.val(Number(qty)*Number(ppu)); else liTotal.val('');

Now for the next part, which is updating the grand totals. My first edition used a while loop. I bet jQuery can do it better:

var totalQty = 0; var totalPrice = 0; $("input[@id^='dyn_qty_']").each(function() { if(!isNaN(this.value)) totalQty+=Number(this.value); });

$("input[@id^='total_']").each(function() { if(!isNaN(this.value)) totalPrice+=Number(this.value); });

$("#gtotal_qty").val(totalQty); $("#gtotal_price").val(totalPrice);

Check it out. I use a selector along with the .each operator to say, do this on each instance of what was found. Note that I had to modify my form a bit to use "g" in front of the grand total fields. I did that so the @id^='total_' wouldn't match the grand total line, but just the line items.

I've attached the complete jQuery edition below. Every time I use it I like it a bit more!

<script src="/jquery/jquery.js"></script> <script>

$(document).ready(function() { $("input[@id^='dyn_']").bind("change",function(e) { var myid = this.id; //get the Index var index = myid.replace(/dyn_(qty|ppu)/,""); var qty = $("#dyn_qty"+index).val(); var ppu = $('#dyn_ppu_'+index).val(); var liTotal = $('#total_'+index); //if we have numbers for both, do math, else, nuke result if(!isNaN(qty) && !isNaN(ppu)) liTotal.val(Number(qty)*Number(ppu)); else liTotal.val('');

	var totalQty = 0;
	var totalPrice = 0;
	$("input[@id^='dyn_qty_']").each(function() {
		if(!isNaN(this.value)) totalQty+=Number(this.value);
	});
	$("input[@id^='total_']").each(function() {
		if(!isNaN(this.value)) totalPrice+=Number(this.value);
	});

	$("#gtotal_qty").val(totalQty);
	$("#gtotal_price").val(totalPrice);
	
});

});

</script>

<cfset numItems = 5> <form id="lineitems"> <table> <tr> <th>Product Qty</th> <th>Price Per Unit</th> <th>Total</th> </tr>

<cfloop index="x" from="1" to="#numItems#"> <cfoutput> <tr> <td><input type="text" name="qty_#x#" id="dyn_qty_#x#"></td> <td><input type="text" name="ppu_#x#" id="dyn_ppu_#x#"></td> <td><input type="text" name="total_#x#" id="total_#x#"></td> </tr> </cfoutput> </cfloop>

&lt;tr&gt;&lt;td colspan="3" bgcolor="yellow"&gt;&lt;B&gt;TOTALS:&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;
	&lt;td&gt;&lt;input type="text" name="gtotal_qty" id="gtotal_qty"&gt;&lt;/td&gt;
	&lt;td&gt;&nbsp;&lt;/td&gt;
	&lt;td&gt;&lt;input type="text" name="gtotal_price" id="gtotal_price"&gt;&lt;/td&gt;
&lt;/tr&gt; 

</table> </form>

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Sid Wing posted on 9/10/2008 at 7:38 PM

As always, this padawan bows to his JediMaster! Muchos Grassyness, Ray... Off to go visit your wishlist!

Comment 2 by Sid Wing posted on 9/10/2008 at 11:11 PM

All was well with this (using the JS) til I realized I lost my "NumberFormat" capability to make the total prices look like dollar amounts. I hate re-inventing the wheel, so - 1 Google Search awya from an answer. I found:

http://www.web-source.net/w...

Which had a WONDERFUL JS function for currency formatting a number.

The function looks like:
<code>
function CurrencyFormatted(amount)
{
var i = parseFloat(amount);
if(isNaN(i)) { i = 0.00; }
var minus = '';
if(i < 0) { minus = '-'; }
i = Math.abs(i);
i = parseInt((i + .005) * 100);
i = i / 100;
s = new String(i);
if(s.indexOf('.') < 0) { s += '.00'; }
if(s.indexOf('.') == (s.length - 2)) { s += '0'; }
s = minus + s;
return s;
}
// end of function CurrencyFormatted()
</code>

So my script block looks something like this:

<code>
<script>
function updateFields(x) {
// x is the index value
var qty = document.getElementById('itemQuan'+x).value;
var ppu = document.getElementById('itemUPrice'+x).value;
var liTotal = document.getElementById('itemTPrice'+x);
// if qty and ppu are both numbers, do math - if not - nuke the result
if(!isNaN(qty) && !isNaN(ppu))
liTotal.value = CurrencyFormatted(qty*ppu);
else
liTotal.value = CurrencyFormatted(0);
// now update the grand total
var i = 1;
var totalPrice = 0;
while(thisQty = document.getElementById('itemQuan'+i)) {
qty = document.getElementById('itemQuan'+i).value;
liTotal = document.getElementById('itemTPrice'+i).value;
if(!isNaN(liTotal)) totalPrice += Number(liTotal);
i++;
}
var formattedTotal = CurrencyFormatted(totalPrice);
document.getElementById('fldReqTotalPrice').value = formattedTotal;
}
function CurrencyFormatted(amount)
{
var i = parseFloat(amount);
if(isNaN(i)) { i = 0.00; }
var minus = '';
if(i < 0) { minus = '-'; }
i = Math.abs(i);
i = parseInt((i + .005) * 100);
i = i / 100;
s = new String(i);
if(s.indexOf('.') < 0) { s += '.00'; }
if(s.indexOf('.') == (s.length - 2)) { s += '0'; }
s = minus + s;
return s;
}

</script>
</code>

Comment 3 by Pete posted on 9/18/2008 at 7:25 PM

Hi there

Just wondering if you had a working example of the form anywhere - I think its what I am trying to do but struggling with the form.

i.e. in the post - Dynamically updating line items in a form

Comment 4 by Sid Wing posted on 9/18/2008 at 7:31 PM

@Pete
If you contact me via e-mail I can send you the code - the application is an "internal use only" app - so it has no real world availability

Comment 5 by Sid Wing posted on 9/18/2008 at 7:32 PM

Helps if I give you the address:

sid dot wing at gmail dot com

Comment 6 by Pete posted on 9/18/2008 at 7:36 PM

Hi there

I would send an email - but dont know the email address. Where do I find your email address. I'll explain exactly what I am trying to do.

Many thanks

Comment 7 by Sonya posted on 2/11/2009 at 9:17 PM

How can I make the "numItems" dynamic in this example instead of hard coded. I am working on a project where i can type in the number of rows i want and then it will spit out that many 3-column rows to add input into. I dont need to calculate anything from there. Just merely submit the inputed data. But I want the page to not have to refresh, just the amount of rows available to change via the number they input. Is this possible with Javascript?

Comment 8 by Raymond Camden posted on 2/11/2009 at 9:22 PM

numItems would have been the number of items being edited which would have come from the database. So it would simply be: somequery.recordCount

Your next question leads me to believe that you are working with NEW data though. If so, then yes, you can use jQuery to dynamically add more rows to a table.

Is that what you want?

Comment 9 by Sonya posted on 2/11/2009 at 10:35 PM

Yes. It is new data. And yes, if you could give me an example of how to accomplish this, that would be great. Here is what i was trying to do so far...

<script type="text/javascript">
function ReloadEntry()
{
var NewText = document.getElementById("noc").value;
var VariableElement = document.getElementById("TextDisplay");
if (NewText =="")
VariableElement.innerHTML = 0;
else
VariableElement.innerHTML = NewText;
}
</script>

<cfform id="eventadd">
<table>
<tr>
<td>Number of Rows:</td>
<td><input id="noc" name="noc" type="text" maxlength="3" size="5" onKeyUp="ReloadEntry();"></td>
</tr>
<cfset noc = 0>
<cfset noc = ??Javascript??>
<cfif variables.noc gt 0>
<tr>
<td>Date</td>
<td>Label</td>
<td>Alternate Name</td>
</tr>
<cfloop index="x" from="1" to="#noc#">
<tr>
<td><cfinput name="date#x#" type="text" maxlength="10"></td>
<td><cfinput name="label#x#" type="text" maxlength="10"></td>
<td><cfinput name="altname#x#" type="text" maxlength="500"></td>
</tr>
</cfloop>
</cfif>
</table>
</form>

Comment 10 by Raymond Camden posted on 2/13/2009 at 7:47 PM

Sonja, I Googled, but didn't find a real nice example of adding dynamic form fields. I'll see if I can blog something myself a bit later today, or this weekend. It is now on my list of things to blog.

Comment 11 by Sonya posted on 2/18/2009 at 9:18 PM

Thanks so much :) You are awesome! Let me know when the example is ready.

Comment 12 by Raymond Camden posted on 2/19/2009 at 11:06 PM

Let me know if this helps: http://www.coldfusionjedi.c...

Comment 13 by Sonya posted on 2/19/2009 at 11:17 PM

The Force Is Truely With You! :) I looked at this and it looks pretty straight forward. I will try to implement it and let you know how it goes as soon as I get back to that page of code. Thanks again! I'm going to make the "Jump to Lightspeed" on this project thanks to you.