Sync WooCommerce Product Variation Menus

I was doing work on a WordPress store where the designer had arranged WooCommerce variation single products using Visual Composer. Visual composers are very popular these days and offer a GUI to easily place modular content inside grid layout containers. This affords designers great control over content and widget placement but not much control when it comes to inter-behavioral module communication1. The client had a site which required additional behavior coupled between variation <select> <option>s of a few different WooCommerce products which had already been placed in different areas of a page using Visual Composer. This was an interesting request because in the visual composer WooCommerce products become “single products” which under normal conditions are isolated with their own “Add to Cart” buttons and each is placed into the cart by a separate user action and without respect to any other products in the layout2. When a user selected a variation <option> from the pages top product the client wished to synchronize the drop down <select> <option>s of the products lower on the page to mirror the value of the selected <option>. This behavior is one way so the user may explicitly change the value of variations on lower products without effecting previous selections on other products.

I was doing work on a WordPress store where the designer had arranged WooCommerce variation single products using Visual Composer. Visual composers are very popular these days and offer a GUI to easily place modular content inside grid layout containers. This affords designers great control over content and widget placement but not much control when it comes to inter-behavioral module communication1. The client had a site which required additional behavior coupled between variation <select> <option>s of a few different WooCommerce products which had already been placed in different areas of a page using Visual Composer. This was an interesting request because in the visual composer WooCommerce products become “single products” which under normal conditions are isolated with their own “Add to Cart” buttons and each is placed into the cart by a separate user action and without respect to any other products in the layout2. When a user selected a variation <option> from the pages top product the client wished to synchronize the drop down <select> <option>s of the products lower on the page to mirror the value of the selected <option>. This behavior is one way so the user may explicitly change the value of variations on lower products without effecting previous selections on other products.

Basic example, when a user selects a color from the main single product at the top of the page it should trigger selection of the same color across any other single products lower on the page which contain the variation attribute "color".The products are related in the real world so the choices made on the lower single products would usually reflect selections made on the main (top) single product, dimensions, size, color, ect.. If the user changed the selected color on the lower single product later it would not trigger changes on any other single products color <select> element. You could think of this as almost trickle down logic or a one to many relationship between a master product and products under its control. If we were to build this type of functionality using an MV application then we would probably impose a 1 way data bind between the top product on the page to any other products on the page. However layering this logic to a WordPress WooCommerce project it is wise to use jQuery for event management and keep our JavaScript in either a plugin or child theme. To make it easier to follow I created a small diagram displaying hypothetical actions and outcomes.

A diagram displaying behavior after we sync woocommerce products. Shows how changes to a single product A, B or C effect each other. Key concept being that only product A effects B and C

JavaScript makes it simple to listen to a change event on an HTML select element and programmatically change the "selected" attribute value on


WooCommerce Custom JavaScript Events

WooCommerce emits custom JavaScript events as part of their variable product management flow and we can’t disrupt that flow. WooCommerce custom JavaScript events let WooCommerce track when it needs to update product inputs with variation values, check forms over for completeness, figure out if enough variable data is selected to warrant price re-calculation and many of other things. WooCommerce triggers these custom events and changing <select> elements programmatically can interrupt this process and even prevent items from properly being added a product to cart. I’ve seen sites where WooCommerce almost hijacks <option> element management from the DOM and detaches and re-populates <select> elements upon user focus and detachs variable options after a selection. Basically, it won’t except just any old programmatic changes without a little effort and just because we can change things with JavaScript easily on the outside, we might find that features such as add to cart don’t work correctly if we don’t remember to update WooCommerce of our changes. So we can’t circumvent these events or our changes won’t propagate and we wouldn’t want to go around WooCommerce anyways, we want to work with WooCommerce functionality, not against it.

Writing the code to Sync WooCommerce Product Variations

Alright, let’s get started on building this solution. If you just want the finished version scroll to the bottom of the post.

1
2
3
4
5
6
7
8
9
var $masterForm = $('form.variations_form.cart[data-product_id="30"]');
jQuery(function($) {
if ($masterForm.length) {
var $masterSelects = $masterForm.find(
'select[data-attribute_name="attribute_size"], ' +
'select[data-attribute_name="attribute_branding"], ' +
'select[data-attribute_name="attribute_size"])');
$masterSelects.on('change.sync-select', syncSelectOptions);
}

In the code above we have checked the DOM for our main product variation form form.variations_form.cart[data-product_id="30"] and selected from that all the variation select elements which will impose their selected options on lower products on the page after a JavaScript change event. With WordPress we usually don’t have to worry about our DOM elements being overwritten with Ajax so we can safely listen directly on $masterSelects for change events. However, using WordPress your JavaScript will most likely be loading on pages other than the page with our variation product form so it’s wise to do a check against the $masterSelects elements in the DOM using the jQuery objects length property.

Notice that I used 'change.sync-select' as the event listener namespace, this could just as well be change.bippity-boppity-boop. It’s a custom namespace on the event type which will allows us to easily manage our own events because the namespace will distinguish our events from other peoples code if we need to do finer event management and is generally good practice I feel. (You can opt out of using namespaces but I find they increase readability, make event cleanup simple and make event triggering more useful). We can start to write our event handler which is going to fire when the user changes anything in the main product variation form $masterForm.

1
2
3
4
5
6
7
function syncSelectOptions(evt) {
var $selectBox = $(this),
val = $selectBox.val( ),
optionType = $selectBox.data( 'attribute_name' );
// Find the forms that we want to sync with our master form
var $sisterForms = $('form.variations_form[data-product_id!=30]');

Now we’ve started our event handler. When a change event is fired our handler will be able to reference the changed select element on $(this). We’ll store the selected value of the variation to val and use it to set the lower products on the page to have that same variation choices on their drop-downs when the user moves down the page. The optionType variable grabs the dataset attribute "attribute_name" from the drop-down so that we can know which <select> element to make changes to in the other variable product forms. Keep in mind that not all products might share the same variations and so we should be careful about how we run through them and never assume that they exist. The variable $sisterForms will hold all the which we want to make changes to. I used a != selector but you could also write a more specific selector if you don’t want to check every other variable product form on the page. Just remember if you don’t exclude your main form then you’ll most likely end up in an endless loop and crash the page because our handler would in fact be triggering the same event which it handles. That’s no good.

1
2
3
4
5
6
7
if ($sisterForms.length) {
$sisterForms.each(function() {
var $form = $(this),
$select = $form.find( 'select[data-attribute_name="'+optionType+'"]' );
if (!$select.length) {
return;
}

Again we check that there are in fact $sisterForms.length before trying to iterate through an empty set. If any sister forms exists on the page then we can use the jQuery each function to run through each product on those forms individually. We’ll check that they contain a <select> element having the same data-attribute_name property as the select element triggering the current event from our main product form and if not then just return from the current each() iteration which will then handle any other form in the $sisterForms jQuery set.

1
2
3
4
5
6
7
// First we will tell the form that we've focused in so that it resets variations.
$form.trigger( 'woocommerce_variation_select_focusin' );
$select.find( 'option' ).each(function() {
$(this)
.prop('selected', $(this).val() == val);
});

Line 26 is an important event that WooCommerce normally triggers on a form when a user focuses on a variation select box. If we don’t trigger('woocommerce_variation_select_focusin') on the <select> element that we wish to change and the user has already changed this <select> element by hand it is possible that all the <option>s could be detached and unavailable to us through $select.find('option') because WooCommerce may have detached all the other <option>s. When we trigger() the woocommerce_variation_select_focusin WooCommerce repopulates the drop-down as it would had the user clicked on the <select> element themselves. Trying to handle this using a trigger('click') event won’t work.

After that all we have to do is iterate through the variation options in the select using $select.find( 'option' ).each and change the ‘selected’ property on each <option> to reflect the state we wish to impose (truthy or falsy). We do this simply by using a loose comparison == of its value to the value of the variation on the main product which you’ll recall we stored as val in the outer scope of our event handler.

1
2
3
4
// Trigger a change which will force WooCommerce to "check_variations"
$select.trigger('change');
});
}

Last but not least we want to fire a change event on the <select> box which will be picked up by WooCommerce so that it can make adjustments to the variation form as if the user had made a selection on each dynamically effected product drop-down themselves. Without doing this the product would show that changes had been made to the <select> boxes but when the user clicked “add to cart” the items would not show the dynamically effected changes.

I hope this tutorial helped you out, I know that working with WordPress can get hairy sometimes since there are so many plugins and it can get tough to track down the right solution. Sometimes we have to dig into plugins and figure out what they need out of us before we can get what we need out of them. Here is the final code, if you use it on your site you should only have to change jQuery selectors to reflect the products on your page. Using ajax add to cart plugins works with this as well. The site I worked also required creating a global “add to cart” button which would iterate through each item on page and add it to cart if possible before actually bringing the user to the checkout page.

The final code to Sync WooCommerce Product Variations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var $masterForm = $('form.variations_form.cart[data-product_id="30"]');
jQuery(function($) {
if ($masterForm.length) {
var $masterSelects = $masterForm.find(
'select[data-attribute_name="attribute_size"], ' +
'select[data-attribute_name="attribute_branding"], ' +
'select[data-attribute_name="attribute_size"])');
$masterSelects.on('change.sync-select', syncSelectOptions);
}
function syncSelectOptions(evt) {
var $selectBox = $(this),
val = $selectBox.val( ),
optionType = $selectBox.data( 'attribute_name' );
// This is a mask for every form but our master. You might want to be
// more specific about which forms will be effected.
var $sisterForms = $('form.variations_form[data-product_id!=30]');
if ($sisterForm.length) {
$sisterForms.each(function() {
var $form = $(this),
$select = $form.find( 'select[data-attribute_name="'+optionType+'"]' );
if (!$select.length) {
return;
}
// First we will tell the form that we've focused in so that it resets variations.
$form.trigger( 'woocommerce_variation_select_focusin' );
$select.find( 'option' ).each(function() {
$(this)
.prop('selected', $(this).val() == val);
});
// Trigger a change which will force WooCommerce to "check_variations"
$select.trigger('change');
});
}
}
});

[1] Inter-behavioral module communication - Made up term to describe logic which could bridge “intertwine” the behavior from one module to effect another module on the page. In this article the logic is similar to 1 way data binding from the values of one form to another.

[2] Without intervention adding WooCommerce single products to a page with visual commerce will give each product its own “add to cart” button that will place only that single product with its’ variation data to the users cart. So 5 single products under normal circumstances yields 1 “Add to Cart” button with each product for a total of 5 buttons rather than having a single button which would be more beneficial for products containing coupled behavior. Also when adding a product to cart by default the page reloads. I added Ajax Add to Cart functionality through a plugin to solve that and hid all the single-product “Add to cart” buttons and then created a button at the bottom of the page which added any products the user selected complete variations for into their cart and redirected them over to the “cart/basket” page.