The ultimate descrambler for Shopify variants

The goal of this tutorial is to show how to “descramble” a long list of variants in Shopify using JavaScript.

The code will work for any number of variants. It will also work for any number of categories of variants. For example, you could sell a pen with these three categories of “variation”: quantity, precision in points (fine, extra-fine, etc.), and color. Other products you sell may hold a different list of options for quantity, type and color — and that's all good. Some other products may have different categories of variants altogether, or just more categories. Still all good. The code will actually parse through the variants titles and create select elements based on what is available on a per-product basis.

The following is not a screenshot. Pick something.

Hard to find anything in there. JavaScript will simplify that.

We want to present visitors with one drop-down select box per hard attribute your product has (for example, one for quantity, and one for color...). By hard attribute, I mean an attribute that justifies the creation of Shopify variants — any attribute that affects pricing. In our little example case, we assume that the color does affect pricing. But it is also possible to add to the mix a pure JavaScript attribute for color, as I explain in this other tutorial.

Here is a way to select a variant that is much easier on the eye:

This method uses JavaScript and is cosmetic. With JavaScript disabled in the browser, your visitors will still be able to pick any variant they want but will need to locate it in your list. If you do have a long list of variants, this tutorial may prove useful. If not... what are you doing here? ;-)

A shop has been created to which the modifications presented in this tutorial have been applied. The theme is based on the caramel version of the Vogue theme by Jared Burns. Feel free to visit the 11heavens Popsicles website and view its source. If you use Firefox and have the Firebug add-on, type the word Descrambler in your console. This object holds a lot of information and will help you debug your own code. We will create and use the Descrambler object in this tutorial. If you do not know what a JavaScript object is, don't worry. It will all become clear as you read along.

A store that sells popsicles

Let's start with a context, as a context makes the whole process fun. You want to sell frozen sweets. Each frozen sweet has its variants. Generally-speaking, these variants specify a quantity, and a flavor — and some sweets come with the option of being sugar-free. The following is a screenshot of the popsicle product page as it is now.

Your product page showing all variants without JavaScript

To be fair, there could be much more variants here. I was limited to 10 SKUs when I set up my fake shop for this tutorial. Still, seeing these 8 options in a table next to 8 radio buttons is not very pretty.

What we'd aim for would be something like this:

Your product page showing variants with JavaScript

The algorithm is super simple, and the JavaScript code needed to implement it will be easy too — easy enough.

  1. Hide the current selection box. (I say hide, not remove.)
  2. To the page, add select boxes and their labels based on reading the titles of all variants.
  3. Add a placeholder element to the page where we will display the price for the current selection.
  4. Add a listener to the new select elements so that whenever a visitor picks something from either of these boxes, we update the price displayed on the page and select the corresponding variant in the hidden select box. That hidden box will still be part of the document and its value will get submitted with the form when the customer clicks on the Add to Cart button.

That's it and all. Now, let's get to it.

Using the jQuery library

I will be using the jQuery library in this tutorial, but use any JavaScript library you know and love. If you don't know any library, I strongly recommend picking jQuery because it is easy to learn and very light-weight. The latest release of jQuery on this day (September 26th, 2008) is 1.2.6. But go ahead and download the latest release, whatever it is at the time you read this.

You will need to upload the library to your Shopify theme Assets directory.

Once you've done this, you will be able to link to it from your theme.liquid template, by adding the Liquid Output {{ 'jquery-1.2.6.min.js' | asset_url | script_tag }} to the head element of your XHTML document, like so:

<head>
...
{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
...
</head>

Your theme may already use another library like Prototype or Mootools. That's not a problem, as you can force jQuery to play nice with other JavaScript libraries. To make it play nice, all you need to do is add one line of JavaScript code in your theme.liquid template, and then use the jQuery namespace whenever you'll write jQuery code.

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
<script type="text/javascript"> 
  jQuery.noConflict();
</script>

This effectively gives the use of the alias $ back to JavaScript libraries that make use of it. For the full, technical explanation, head over here.

In addition to using the jQuery library, we will use one jQuery plugin to aid in the creation of our select boxes. Mind you, this can all be done 'by hand' but let's make our job as easy as possible. Go ahead and download the Select box manipulation plugin. Pick the packed version, its weight is only 2.9 KBytes. The documentation for the plugin is on this page.

To use this plugin, upload it to your Assets directory, and link to it in your theme.liquid template.

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
{{ 'jquery.selectboxes.pack.js' | asset_url | script_tag }}
<script type="text/javascript"> 
  jQuery.noConflict();
</script>

We are now ready to implement our JavaScript solution.

Working on the product pages

Although we will want the browser to execute our JavaScript code on product pages only, we will not add our JavaScript code to product.liquid. We could, but we prefer to add all JavaScript to the head element of the document — where we can find all code in a snap, even when tired and confused. Open up your theme.liquid template, and add this Liquid condition to it: {% if template == 'product' %}. We also want to modify the document when it's ready, so we'll wrap everything in a function that will be executed when that happens.

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
{{ 'jquery.selectboxes.pack.js' | asset_url | script_tag }}
<script type="text/javascript">
  jQuery.noConflict();
  {% if template == 'product' %}
  jQuery(function() {
    /* All our descrambling code will be here */
  });
  {% endif %}
</script>

The following...

 $(function(){
   // Your code here
 });

... is a shorthand for...

 $(document).ready(function(){
   // Your code here
 });

Using a JavaScript Object to hold all of our stuff

We'll create a JavaScript Object to hold the info we read about our variants. This Object will also define functions we'll call, one after the other, to parse the variants titles, then build select boxes and then update the document appropriately when a visitor makes a selection. We will not create an Object to look like cool rock-on JavaScript programmers, but to make it easier to debug our code using Firebug.

Amend the above JavaScript to include the new Descrambler Object.

  jQuery.noConflict();
  {% if template == 'product' %}
  Descrambler = {
    // We will define a bunch of properties here,
    // among which there will be at least 5 functions.
    // Did you know that everything is a property
    // of an object in JavaScript? 
    // A function is a property too.
    // @see this article:
    // http://11heavens.com/everything-is-a-property-of-an-object-in-javascript
  }
  jQuery(function() {
    // Here, we'll call functions of the Descrambler object.
    // One after the other.
    // To get the job done.
    Descrambler.hideCurrentSelectionBox();
    Descrambler.buildAttrLists();
    Descrambler.addSelectBoxes();
    Descrambler.addPriceBox();
    Descrambler.addListenerOnSelection();
  });
  {% endif %}

Let's work a little harder and define some properties for our Object. We will initialize some properties as we define them. For now, functions will be empty shells: they will do nothing. We will fill up the gaps later on.

  jQuery.noConflict();
  {% if template == 'product' %}
  Descrambler = {
    delimiter: ' - ', // This is a sequence of characters. A String.
    variants: {}, // Object containing info on variants.
    totalAttr: 0, // Number of attributes for the product.
    hideCurrentSelectionBox: function() {
      // Hide the unsightly select box. Yuck.
    },
    buildAttrLists: function() {
      // Come up with lists of attributes.
      // We will update the totalAttr variable here.
    },
    addSelectBoxes: function() {
      // Add selection boxes to the page.
    },
    getCurrentSelection: function() {
      // Return the variant title that corresponds to what is currently selected.
    },
    addPriceBox: function() {
      // Add a placeholder element to display the price.
    },
    addListenerOnSelection: function() {
      // Add a listener to the new select boxes.
      // When visitor makes a selection, modify both the price 
      // and the current selection on the hidden form element.
    }
  }
  jQuery(function() {
    Descrambler.hideCurrentSelectionBox();
    Descrambler.buildAttrLists();
    Descrambler.addSelectBoxes();
    Descrambler.addPriceBox();
    Descrambler.addListenerOnSelection();
  });
  {% endif %}

We need a delimiter.

When our JavaScript will read each variant title, it will need to know where one attribute name ends and an other begins. A naming convention for the variant title will solve that problem for us. We need a delimiter. The delimiter can be anything at all, except a character that'll possibly get included within an attribute name (such as a space). However, the delimiter must look good, not be something tacky like *, because the delimiter will be part of the variant title, hence displayed on the cart page &mdash and shown on product pages to visitors who browse without JavaScript. The following delimiter is good enough:  - . Space-hyphen-space. Pick what you want. Just respect your naming convention whenever you fill up that variant title field while creating new products. And amend the code to reflect your choice. That's a single line of code to edit.

delimiter: ' - ', // This is a sequence of characters. A String.

Displayin' none that unsightly select box

We won't remove the current list of variants. We will simply hide it. Later on, after we've added our own select boxes, we'll want to update the current selection in the hidden "box" whenever the visitor makes a selection in the new boxes. Our new boxes will be just for show. None of their values will be submitted with the Add to Cart form — in other words.

In the Vogue theme, the variants are radio elements. Radio elements come with labels, and wishing to hide all of this, I will select the list that contains these radios and labels, and apply jQuery.hide() to it, like so:

hideCurrentSelectionBox: function() {
  jQuery('#product-variants ul').hide();
},
jQuery.hide() applies the following CSS to what is selected: display:none;.

Building list of attributes

We will read each variant title and split it in parts using our delimiter. We will put the first part in one bag (a first Array we'll call attributes_0), then we'll put the 2nd part in a 2nd bag (Array named attributes_1), third part in third bag, etc. for as many parts as the variant title contains. Before we blindly put a part in a bag, we'll look into the bag to see if a part with the same name is not already in there.

Additionally, while we loop through the product variants, we'll record information about each variant: the variant's title, its unique ID, and its price. We will need to refer to this information later on. We will use the following Object to hold our information: Descrambler.variants.

buildAttrLists: function() {
  var firstTitle = jQuery.trim('{{ product.variants.first.title }}');
  this.totalAttr = firstTitle.split(this.delimiter).length;
  for (var i = 0; i < this.totalAttr; i++) {
    // Initializing each Attributes Array.
    this['attributes_' +i] = [];
  }
  var split = []; // Initializing a split result.
  {% for variant in product.variants %}
  var variantTitle = jQuery.trim('{{ variant.title }}');
  this.variants[variantTitle] = {};
  this.variants[variantTitle].id = '{{ variant.id }}';
  this.variants[variantTitle].price = '{{ variant.price }}';
  split = variantTitle.split(this.delimiter);
  for (var j = 0; j < split.length; j++) {
    // If the attribute option is not yet recorded to its Attribute Array.
    if (jQuery.inArray(split[j], this['attributes_' + j]) == -1) {
      this['attributes_' + j].push(split[j]);
    }
  }
  {% endfor %}
},

The native JavaScript method String.split() is used to split a String into an Array of Strings. The syntax is String.split(separator, howmany). The first parameter is a regular expression, or a string, used to determine where the split must occur. The second parameter is optional, it is a number, and limits the number of parts returned.

jQuery.trim() is used to get rid of any space that could have been inserted by accident at the beginning or end of the variant title, something we often do when copying and pasting variant titles in the Shopify Admin UI.
jQuery.inArray() is a jQuery utility function we'll use to make sure there are no duplicates in our attributes_n Arrays.

From jQuery in Action

Taken from jQuery in Action

Adding select boxes

The API of the jQuery plugin Select box manipulation is quite simple. The matter in which we add options to a select box is demonstrated here:

var myOptions = {
  "Value 1" : "Text 1",
  "Value 2" : "Text 2",
  "Value 3" : "Text 3"
}
$("#myselect").addOption(myOptions);

In our case, value and text will be the same. We will create each select box with jQuery('<select></select>').

addSelectBoxes: function() {
  for (var i = 0; i < this.totalAttr; i++) {
    var options = {};
    for (var j = 0; j < this['attributes_' +i].length; j++) {
      var option = this['attributes_' +i][j];
      options[option] = option;
    }
    var labels = ['Choose a quantity:', 'Choose a flavor:', 'Sugar or no sugar:'];
    jQuery('<label for="select_' + i + '"></label>').html(labels[i]).appendTo('#product-variants');
    jQuery('<select></select>').addOption(options).attr('id', 'select_' + i).appendTo('#product-variants').find('option:first').attr('selected', 'selected');
  }
},

We start by creating the select box, then we add options to it, then we give it an id attribute, append it to the div with id 'product-variants', and, it's not over, we select the first option and mark it as selected. Using jQuery, we can chain all these actions, we can make it all happen with one single instruction, that is, one line of code followed by a semicolon.

What variant title is being chosen

We need a utility function, one that will read the values of our select boxes and reconstruct from these values the title of a variant.

getCurrentSelection: function() {
  var currentValues = [];
  for (var i = 0; i < this.totalAttr; i++) {
    currentValues.push(jQuery('#select_' + i).val());
  }
  var currentVariantTitle = currentValues.join(this.delimiter);
  return currentVariantTitle;
},
In JavaScript, the reverse of a split is a join. So we've used the function Array.join(separator) to re-construct the variant title that corresponds to the current selections. How do we read the selected value of a select element? We read it in the same way as we read the value of an input element, we use jQuery(selector).val().

How much does this cost

We will add a placeholder element to the page in which we will display the price of the currently selected variant. We will put something in this placeholder right away, we will initialize its value.

addPriceBox: function() {
  var firstVariantTitle = '{{ product.variants.first.title }}';
  var price = (parseFloat({{ product.variants.first.price }}) / 100);
  jQuery('<p></p>').attr('id', 'variant_price').html('Base price: $' + price.toFixed(2)).appendTo('#product-variants');
},

Updating the price and the variant selection

We will now add a listener to the new select elements so that whenever a visitor picks something from either of these boxes, we update the price displayed on the page and select the corresponding variant in the hidden select box. That hidden box will still be part of the document and its value will get submitted with the form when the customer clicks on the Add to Cart button.

addListenerOnSelection: function() {
  jQuery('select').change(function() {
    var currentVariantTitle = Descrambler.getCurrentSelection();
    var newId = Descrambler.variants[currentVariantTitle].id;
    var newPrice = Descrambler.variants[currentVariantTitle].price;
    newPrice = parseFloat(newPrice) / 100;
    // Here we select the radio button which corresponds to our variant id.
    jQuery(':radio[name=id]').val(newId);
    // Here we update the price shown on the page.
    jQuery('#variant_price').hide().html('New price: $' + newPrice.toFixed(2)).fadeIn('slow');
    return true;
  });
}

The price will be updated when a new selection is made.

The price will be updated when a new selection is made.

And we're done.

Something not working? Time to test

Click on the Firebug icon at the bottom right of your Firefox navigator window, or press F12.

The firebug icon in Firefox


Open your console. Then, type Descrambler in the right portion of the Firebug panel.

The Firebug console


Then press Run.

The run command


Expand the Descrambler Object by clicking on it. We'll examine its content.

Examining the content of Descrambler


Here is what I see for the Popsicles product.

The content of Descrambler

Things to look for:

Common mistakes:

In conclusion

If you've used this tutorial for your shop, consider dropping me a thank you line here, and/or donate to my website. I don't work for Shopify. No one is paying me to write these tutorials. I am doing this to help the Shopify community. Ironically, I am doing myself a disservice, because if I show you how to do these things, and do this well enough, then you won't need to hire me do do these things for you ;-) Thanks.