| Both sides previous revisionPrevious revisionNext revision | Previous revision |
| v:2.0:snippets:updating_total_price_with_option_modifers [2017/04/26 07:02] – external edit 127.0.0.1 | v:2.0:snippets:updating_total_price_with_option_modifers [2023/07/12 10:38] (current) – [Dynamically triggering recalculation] adam |
|---|
| ====== Updating a total element to reflect selected price modifiers in your page ====== | ====== Updating a total element to reflect selected price modifiers in your page ====== |
| |
| ===== Step 1: Include jQuery ===== | It can be useful to display a dynamically update total cost to customers on your add to cart pages - to let them know what the options they're selecting will cost. The following snippet will allow you to display a dynamic total to customers, taking into consideration a change in quantity or options that include a price product option modifier. |
| Firstly, ensure you're manually including jQuery on your page - and not relying on loader.js to include it for you. You can include it like this: | |
| <code html> | |
| <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> | |
| </code> | |
| |
| ===== Step 2: Include snippet ===== | ===== Step 1: Include snippet ===== |
| Paste the following right after the jQuery include of any page that requires FoxyCart attribute modifiers calculations. | Paste the following before the closing ''</body>'' tag of any page that you want to display the dynamically calculated price of a product. It can also be included in an existing javascript file you're including if you prefer. This snippet relies on the ''loader.js'' file being included on your page. |
| <wrap hi> This is *not* required on the cart or checkout pages, so should not be place in those templates. </wrap> | |
| |
| <code javascript> | <wrap hi> This is *not* required on the cart, checkout or receipt pages, so should not be place in those templates. </wrap> |
| <script type="text/javascript" charset="utf-8"> | |
| jQuery(document).ready(function() { | |
| TOTALCLASS = "_total"; | |
| // Create an array to store all the adjustments | |
| ADJUST = []; | |
| // Set the handlers for each *_adjust form element | |
| $("[class$=_adjust]").each(function() { | |
| var pID = $(this).attr("class").split("_")[0]; | |
| if (typeof(ADJUST[pID]) === "undefined") { | |
| // Create the array for this product code | |
| ADJUST[pID] = []; | |
| } | |
| switch(this.tagName) { | |
| case "SELECT": | |
| // Store the current value | |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | |
| $(this).change(function() { | |
| var pID = $(this).attr("class").split("_")[0]; | |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | |
| recalcTotal(); | |
| }) | |
| break; | |
| case "INPUT": | |
| switch($(this).attr("type")) { | |
| case "checkbox": | |
| if ($(this).is(":checked")) { | |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | |
| } else { | |
| ADJUST[pID][$(this).attr("name")] = ""; | |
| } | |
| $(this).bind("change", function () { | |
| var pID = $(this).attr("class").split("_")[0]; | |
| if ($(this).is(":checked")) { | |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | |
| } else { | |
| ADJUST[pID][$(this).attr("name")] = ""; | |
| } | |
| recalcTotal(); | |
| }) | |
| break; | |
| case "radio": | |
| // Store the current value | |
| if ($(this).is(":checked")) { | |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | |
| } | |
| |
| $(this).bind("change", function () { | <code javascript> |
| var pID = $(this).attr("class").split("_")[0]; | <script> |
| ADJUST[pID][$(this).attr("name")] = $(this).val(); | // Dynamic Price Calculation v3.3 |
| recalcTotal(); | var pricemod_regex=/[{\|]p([+\-:])([\d\.]+)(?:\D{3})?(?=[\|}])/,id_regex=/^(\d+):/,FC=FC||{};function initDynamicPrice(){ADJUST={},$("input,select").off("change.foxy-dynamic-price"),$('form[action*="'+FC.settings.storedomain+'"]').each((function(){var t=$(this),a="",e={products:{}};$(this).find("[name='name'],[name^='name||'],[name$=':name'],[name*=':name||']").each((function(){var r=getId(this.name),i=r?r+":":"",s=parseFloat(t.find("[name='"+i+"price'],[name^='"+i+"price||']").first().val()),c={id:r,code:"",base_price:isNaN(s)?0:s,quantity:1,attributes:{},has_quantity:!1},n=t.find("[name='"+i+"quantity'],[name^='"+i+"quantity||']"),o=t.find("[name='"+i+"code'],[name^='"+i+"code||']");if(o.length>0&&(c.code=clearHash(o.first().val()),""===a&&(a=c.code)),n.length>0){var d=0,h=getElementType(n);["select","text"].indexOf(h)>-1?(c.has_quantity=!0,d=parseFloat(clearHash(n.val()))):["radio","checkbox"].indexOf(h)>-1&&(c.has_quantity=!0,1==n.filter(":checked").length&&(d=parseFloat(clearHash(n.filter(":checked").val())))),isNaN(d)&&(d=0),c.quantity=d}e.products[r]=c}));t.attr("data-fc-form-code")&&(a=t.attr("data-fc-form-code")),""!==a&&($(this).find("input,select").each((function(){if(!this.disabled){var t=getId(this.name),r=getName(this.name),i=getElementType($(this));if("quantity"!=r)if("price"!=r||"hidden"==i){if("SELECT"==this.tagName){var s=!1;$(this).children("option").each((function(){this.value.search(pricemod_regex)>-1&&(s=!0)})),s&&($(this).data("fc-adjust-for",a),e.products[t].attributes[clearHash(this.name)]=clearHash(this.value),$(this).on("change.foxy-dynamic-price",(function(){ADJUST[$(this).data("fc-adjust-for")].products[t].attributes[clearHash(this.name)]=clearHash(this.value),recalcTotal()})))}else if(this.value.search(pricemod_regex)>-1)switch($(this).data("fc-adjust-for",a),$(this).attr("type")){case"checkbox":$(this).is(":checked")?e.products[t].attributes[clearHash(this.name)]=clearHash(this.value):e.products[t].attributes[clearHash(this.name)]="",$(this).on("change.foxy-dynamic-price",(function(){$(this).is(":checked")?ADJUST[$(this).data("fc-adjust-for")].products[t].attributes[clearHash(this.name)]=clearHash(this.value):ADJUST[$(this).data("fc-adjust-for")].products[t].attributes[clearHash(this.name)]="",recalcTotal()}));break;case"radio":e.products[t].attributes.hasOwnProperty(clearHash(this.name))||(e.products[t].attributes[clearHash(this.name)]=""),$(this).is(":checked")&&(e.products[t].attributes[clearHash(this.name)]=clearHash(this.value)),$("[name='"+this.name+"']").data("fc-adjust-for",a).on("change.foxy-dynamic-price",(function(){ADJUST[$(this).data("fc-adjust-for")].products[t].attributes[clearHash(this.name)]=clearHash(this.value),recalcTotal()}))}}else $(this).data("fc-adjust-for",a).on("change.foxy-dynamic-price",(function(){var a=0;(["select","text"].indexOf(i)>-1||["radio","checkbox"].indexOf(i)>-1&&$(this).is(":checked"))&&(a=parseFloat(clearHash(this.value))),isNaN(a)&&(a=0),ADJUST[$(this).data("fc-adjust-for")].products[t].base_price=a,recalcTotal()}));else $(this).data("fc-adjust-for",a).on("change.foxy-dynamic-price",(function(){var a=0;(["select","text"].indexOf(i)>-1||["radio","checkbox"].indexOf(i)>-1&&$(this).is(":checked"))&&(a=parseFloat(clearHash(this.value))),isNaN(a)&&(a=0),ADJUST[$(this).data("fc-adjust-for")].products[t].quantity=a,recalcTotal()}))}})),ADJUST[a]=e)})),recalcTotal()}function clearHash(t){return t.replace(/\|\|[\d\w]+(?:\|\|open)?$/,"")}function getNameParts(t){return(t=clearHash(t)).match(/(?:(\d+):)?(.*)/)}function getId(t){var a=getNameParts(t);return id_regex.test(this.name)&&(prefix=parseInt(this.name.match(id_regex)[0])),void 0===a[1]?0:parseInt(a[1])}function getName(t){return getNameParts(t)[2]}function getElementType(t){if("SELECT"==t[0].tagName)return"select";if("INPUT"==t[0].tagName)switch(t.attr("type").toLowerCase()){case"text":case"number":case"tel":return"text";default:return t.attr("type").toLowerCase()}}function recalcTotal(){for(f in ADJUST){var t=0,e=0;for(p in ADJUST[f].products){var r=ADJUST[f].products[p].base_price,i=0;for(a in ADJUST[f].products[p].attributes){var s=ADJUST[f].products[p].attributes[a].match(pricemod_regex);if(s)switch(s[1]){case":":r=parseFloat(s[2]);break;case"+":i+=parseFloat(s[2]);break;case"-":i-=parseFloat(s[2])}}r+=i,t+=r*=ADJUST[f].products[p].quantity,e+=ADJUST[f].products[p].quantity}"function"==typeof fcFormatPrice&&(t=fcFormatPrice(t,f)),"function"==typeof fcFormatQuantity&&(e=fcFormatQuantity(e,f)),t=adjustDisplayPrice(t);var c="object"==typeof FC&&FC.hasOwnProperty("json")&&FC.json.config.hasOwnProperty("currency_format")?jQuery.trim(FC.util.money_format(FC.json.config.currency_format,t)):t.formatMoney(2);$("."+f+"_total").html(c),$("."+f+"_total_quantity").html(e)}}function adjustDisplayPrice(t){return"function"!=typeof fc_adjust_price_display||(adjusted_price=fc_adjust_price_display(t),void 0===adjusted_price||isNaN(parseFloat(adjusted_price)))?t:parseFloat(adjusted_price)}FC.onLoad=function(){FC.client.on("ready.done",initDynamicPrice)},Number.prototype.formatMoney=function(t,a,e){var r=this,i=(t=isNaN(t=Math.abs(t))?2:t,a=null==a?".":a,e=null==e?",":e,r<0?"-":""),s=parseInt(r=Math.abs(+r||0).toFixed(t))+"",c=(c=s.length)>3?c%3:0;return i+(c?s.substr(0,c)+e:"")+s.substr(c).replace(/(\d{3})(?=\d)/g,"$1"+e)+(t?a+Math.abs(r-s).toFixed(t).slice(2):"")}; |
| }) | |
| break; | |
| } | |
| break; | |
| } | |
| }); | |
| recalcTotal(); | |
| }); | |
| var FC = FC || {}; | |
| FC.onLoad = function () { | |
| FC.client.on('ready.done', function () { | |
| recalcTotal(); | |
| }); | |
| } | |
| function parseAttributes(sValue) { | |
| sValue = sValue.match(/{(.*?)}/); | |
| if (sValue) { | |
| aValue = sValue[1].split("|"); | |
| for(i in aValue) { | |
| aParts = aValue[i].match(/p([\+|\-|\:])(.+)/); | |
| if (aParts) { | |
| return aParts; | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| function recalcTotal() { | |
| for(p in ADJUST) { | |
| price = Number($("#"+p+"_price").val()); | |
| adjustment = 0; | |
| for (a in ADJUST[p]) { | |
| var aParts = parseAttributes(ADJUST[p][a]); | |
| if (aParts) { | |
| var modifier = aParts[1]; | |
| switch(modifier) { | |
| case ":": | |
| price = Number(aParts[2]); | |
| break; | |
| case "+": | |
| adjustment += Number(aParts[2]); | |
| break; | |
| case "-": | |
| adjustment -= Number(aParts[2]); | |
| break; | |
| } | |
| } | |
| } | |
| price += adjustment; | |
| var fprice = (typeof FC == "object" && FC.hasOwnProperty("json") && FC.json.config.hasOwnProperty("currency_format")) ? jQuery.trim(FC.util.money_format(FC.json.config.currency_format, price)) : price.formatMoney(2); | |
| $("."+p+TOTALCLASS).html(fprice); | |
| } | |
| } | |
| Number.prototype.formatMoney = function(c, d, t){ | |
| var n = this, | |
| c = isNaN(c = Math.abs(c)) ? 2 : c, | |
| d = d == undefined ? "." : d, | |
| t = t == undefined ? "," : t, | |
| s = n < 0 ? "-" : "", | |
| i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", | |
| j = (j = i.length) > 3 ? j % 3 : 0; | |
| return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ""); | |
| }; | |
| </script> | </script> |
| </code> | </code> |
| |
| ===== Step 3: Set up forms ===== | ===== Step 2: Set up forms ===== |
| |
| Setup your forms like so: | Setup your forms like so: |
| * Every FoxyCart based product that have product modifiers needs to have a unique code set like: | ==== Product Code ==== |
| <code html> <input type="hidden" name="code" value="p1" /> </code> | Every FoxyCart based product that you want to dynamically calculate the total needs to have a unique code set like this: |
| * Give your hidden price input an id that combines your code and "_price" like: | <code html><input type="hidden" name="code" value="p1" /></code> |
| <code html> <input type="hidden" name="price" id="p1_price" value="20" /> </code> | |
| * Any select, checkbox or radio element needs to be given a class that combines your code and "_adjust" like: | |
| Selects: | |
| <code html> <select class="p1_adjust" name="Size"> | |
| <option value="10 ml">10 ml</option> | |
| <option value="20 ml{c+test|p+2.50}">20 ml</option> | |
| <option value="20 ml{p+50|c+test}">30 ml</option> | |
| </select></code> | |
| Checkboxes: | |
| <code html><label for="something">Something</label><input type="checkbox" name="something" value="Yes{p:10|c+test}" class="p1_adjust"></code> | |
| Radio Buttons (note that the grouped radio buttons need to all share the same "name") : | |
| <code html> | |
| <input type="radio" class="p1_adjust" name="group1" value="Milk{p:2}"><label>Milk</label> | |
| <input type="radio" class="p1_adjust" name="group1" value="Butter{p:3}" checked><label>Butter</label> | |
| <input type="radio" class="p1_adjust" name="group1" value="Cheese{p:4}"><label>Cheese</label> | |
| </code> | |
| * Set an element to be where the total cost is shown and updated, by combining the code and "_total" as its class, like: | |
| <code html> <h4><span class="p1_total">$20.00</span></h4></code> | |
| The total would be put into that span element, and as no dollar sign is returned with the value, that is placed outside the span in the h4 element. If you want to use a class that is the code combined with a different string, this can be set at the top of the javscript in the TOTALCLASS variable. | |
| |
| | The code from the first product in your form will be used as a key for using in the element that dynamically shows the total product price. |
| |
| | If you're not able to know what product code is first, or you just want to set a specific code to use for the price modifier, you can also add a data attribute to your form like this: |
| | |
| | <code html><form ... data-fc-form-code="p1"></code> |
| | |
| | ==== Total Price Element ==== |
| | |
| | Next, set an element to be where the total cost is shown and updated, by combining the code from above and "_total" as its class, like this: |
| | <code html><h4 class="p1_total">$20.00</h4></code> |
| | |
| | If you'd like to show the total quantity, you can also do that using an element with a class that is a combination of the code and "_total_quantity" as its class, like: |
| | <code html><p class="p1_total_quantity">0</p></code> |
| | |
| | ==== Dynamically triggering recalculation ==== |
| | |
| | If for whatever reason you need to trigger the dynamic price calculations to be recreated - for example if you dynamically add in a new input that includes a price modifier, you can execute this function in your javascript: |
| | |
| | <code javascript>initDynamicPrice();</code> |
| | |
| | ==== Modify dynamically calculated total price ==== |
| | |
| | If you need to dynamically modify the calculated price, for example if your product prices are tax exclusive, but you want to show the calculated price as tax inclusive, you can define a function on your page to achieve that result. The function has a name of ''fc_adjust_price_display'', and is passed a single argument which is the dynamically calculated total price. The function should return your modified price. |
| | |
| | As an example, let's say you want to account for a 25% inclusive tax on your product prices, that would look like this: |
| | |
| | <code javascript><script> |
| | function fc_adjust_price_display(price) { |
| | return price * 1.25; |
| | } |
| | </script></code> |
| ===== That's it! ===== | ===== That's it! ===== |
| |
| That should be it. The script basically runs through all the elements that have a class ending in "_adjust", and grabs what the active value is. If that value has a price modifier (p:X, p+X, p-X), it chucks it into an array. After looping over all the form elements, it loops through all the relevent price modifiers, and adjusts the base price and sets the total element. Worth noting is that if you have multiple flat price modifiers (eg p:30) where it alters the base price, only the last modifier is applied, but any plus or minus modifiers are applied after that. | That should be it. The script basically runs through all the inputs within FoxyCart add to cart forms on the page, and grabs what the active value is. If that value has a price modifier (''p:X'', ''p+X'', ''p-X''), it chucks it into an array. After looping over all the form elements, it loops through all the selected price modifiers, adjusts the base price, multiplies it by the quantity and sets the total element. |
| | |
| | ===== Notes and Limitations ===== |
| | |
| | * The snippet is run after the FoxyCart javascript has been initialized. As such - there may be a small time while the page is loading before the dynamic total is calculated. |