As part of a larger Magento to WordPress and Woocommerce platform switch, we were required to provide functionality that enabled customers to purchase multiple quantities of rubber roofing membrane that was priced per meter, in steps of 0.1 meters, for a variety of prescribed widths.
Although there are a few well known pre-existing plugins that enable measurement based pricing for variable products, in this case we had to develop our own solution as the brief required the customer to be able to select and add multiple quantities of the required width & length to the cart from the product page, wand also keep those individual ordered lengths separate in the cart.
First we add the product to Woocommerce as a variable product, with the prescribed widths as variations. We add the price as the price per meter.
Next we write a function that adds an additional quantity select field for the select length field. We want to add an additional field for this rather than using the existing Woocommerce quantity field, as we need to reserve the actual quantity field for selecting the quantity of cuts to be ordered. The easiest way to add this additional field to the product page is to use the ‘woocommerce_before_add_to_cart_button’ action. This can be called in the functions.php file of your child theme, or added as a plugin.
add_action( 'woocommerce_before_add_to_cart_button', 'add_measurement_elements');
In our case we need to only add this field for a few select products. This action does not pass any product data, so in this case to get the current product id we use the ‘global $product’ object, and then ‘$product->get_id()’, as can be seen below.
Then we echo out two blocks of html to the page. The first block, is a direct copy of the themes quantity fields in our own div, this way we can rely on the themes styling, and script for the plus & minus buttons. We have changed the input’s id and name, to separate it from the existing quantity field, and we have also added a ‘step=”0.1”’ attribute, to enable customers to select lengths of 0.1 meter denominations. The second block of html we add are the fields that we will later use to display the calculated price.
function add_measurement_elements() { global $product; $id = $product->get_id(); $membranes = [17363, 17376, 21870, 17313]; if (in_array($id, $membranes)) { echo ' <div class="lengthselectcont"> <label id="chooselengthlbl">Select length (m): </label> <div class="quantity buttons_added actuallength"><button type="button" value="-" class="minus">-</button> <input type="number" id="actuallength" name="actuallength" class="input-text qty text" step="0.1" value="1" title="Qty"/> <button type="button" value="+" class="plus">+</button></div> </div> '; echo ' <div class="pricecalccont"> <label class="pricecalclbl">Subtotal (price per cut): </label> <label class="pricecalclbl" id="pricecalcresult"></label> </div> '; } } }
At this point we should have these fields render on the required product pages correctly. Next we need to handle the add to cart process so that each of the quantities is added to the cart as a separate cart line item, thus reserving the quantity field in the cart for the length of each individual cut of roofing membrane. For this we will use the ‘woocomerce_add_to_cart’ action.
add_action( 'woocommerce_add_to_cart', 'split_items', 10, 6 );
In our ‘split_items’ function below, we again choose to only apply our actions to our selected products, this time using the product_id that is passed through the action. The first thing we want to do is retrieve our length value. As we have added the field through the ‘woocoomerce_before_add_to_cart_button’ action, we know that the field is in the add to cart form, and therefore the data has been posted back with the rest of the add to cart data, when the customer hits the add to cart button. We can retrieve the posted value using: ‘filter_input( INPUT_POST, ‘actuallength’)’, – actuallength being the ID of the input that we added.
Next we iterate through the quantities, and assign them a unique cart key. We set their quantity to 1 (we will set this to the length value afterwards), and add them to the cart using ‘WC()->cart->add_to_cart()’ function.
It’s important to note that we iterate through the items starting at 1, and finish at quantity-1. The first reason for this, is that our function is adding additional cart items, and isn’t replacing the entire add to cart process. We will get an exception if you try to add more products than the quantity-1.
Finally we use ‘WC()->cart->set_quantity()’ to set the quantity property of the cart items we have just added, to the length value that we retrieved earlier. The reason we initially set the quantity to 1, and then set the quantity to the correct value outside of the loop and after we have added it to the cart, is because the ‘woocommerce_add_to_cart’ action is called every time we use the ‘WC()->cart->add_to_cart()‘ function. So we would end up in a big, infinite, recursive mess if you set the quantity to anything other than 1 initially. This is also the second reason we iterate for quantity-1, as when our function is called recursively with the quantity as 1, it iterates 0 times, and it doesn’t add further items to the cart.
function split_items( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) { $membranes = [17363, 17376, 21870, 17313]; if (in_array($product_id, $membranes)) { $actuallen = 1; $actuallen = filter_input( INPUT_POST, 'actuallength'); for ($i = 1; $i <= ($quantity-1); $i++) { $actuallength = 1; $unique_cart_item_key = md5( microtime() . rand() ); $cart_item_data['unique_key'] = $unique_cart_item_key; WC()->cart->add_to_cart( $product_id, $actuallength, $variation_id, $variation, $cart_item_data ); } WC()->cart->set_quantity( $cart_item_key, $actuallen ); } }
The final part of the development is to write a Javascript function that calculates and displays the price of the selected width & length on the product page.
For this project we used the Porto theme, and the nature of the theme made it difficult to make the displaying and updating of the calculated price event driven. So for simplicity I implemented an asynchronous solution using setInterval(), but in most cases it should be possible to update the price using event listeners.
setInterval(() => { calculateprice(); }, 500);
To calculate the price of the selected length we first need to get the per unit price. For a simple product this part is comparatively simple, but for a variable product it’s more complex. In this case we’re working with a variable product, so we initially need to get the selected variation attribute from the select box.
var selectvar = document.getElementsByClassName('variations')[0].getElementsByTagName('select')[0]; var selectedattribute = selectvar.options[selectvar.selectedIndex].text;
Then we use the selected variation attribute to find the price per unit, which can be found in the product json hidden in the DOM.
var selectedprice = ""; var variationsform = document.getElementsByClassName('variations_form cart vf_init')[0]; var varjson = variationsform.getAttribute('data-product_variations'); var jsonobj = JSON.parse(varjson); jsonobj.forEach(obj => { var attributeobj = obj.attributes; if (Object.values(attributeobj)[0] == selectedattribute) { selectedprice = obj['display_price']; } });
Then we get the selected length from length select field, and calculate the price of the selected width and length. We also round it to ensure that it displays properly as a price.
var length = document.getElementsByName('actuallength')[1].value; var totalpriceunrounded = (selectedprice.toFixed(2)) * length; var totalprice = Math.round((totalpriceunrounded + Number.EPSILON) * 100) / 100
Then we display add our price to the price field that we added at the beginning.
var priceelement = document.getElementById('pricecalcresult'); priceelement.innerHTML = '£' + totalprice.toFixed(2);
To avoid lots of console errors, it’s a good idea to add some handling to ensure that this code only fires for select products after the document is ready. Or use a try and catch.