Beyond Customized 2: Shopify and Javascript

By Dan Turner, Culture Foundry Developer / Thu Sep 14, 2017
The techniques outlined in the previous article allow adding multiple items to the cart at once to bypass product variant limits.

Note: You are reading part two of Beyond Customized: Shopify and Javascript. To read part one please click here: Beyond Customized: Shopify and Javascript

 

But once those items are in the cart what's to stop a shopper from removing some of the components or modifying their quantities in ways which make the order invalid or unfulfillable? To solve that problem we can use a few different techniques to group products in the cart together, and then treat that group as one highly customized product.

Grouping Products

A group of products is a collection of a few different individual products that are inseparable. Let's imagine we're selling RC cars, but not RC car parts or services. If an option for the car is a custom paint job that can't be sold without also selling the car and its constituent parts. Our group might contain 1) car chassis, 2) knobbly wheels, 3) spoiler (extra large), and 4) hot rod paint job. None of these would be sold individually, and it isn't even possible to sell a paint job without selling the chassis on which it would be applied. If all of these are represented by different products to escape the Shopify variant limitations we wouldn't want to allow the customer to remove the chassis but keep the paint job because then the order can't be filled. Grouping and handling those groups via scripting is the solution for this.

First we need a way to identify that items are all of the same group. To do this we'll add a randomly generated ID as a property on all of the products. Here's a function which generates a GUID, which isn't strictly necessary here but gives us a nice big random string which is very unlikely to collide.

var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
function generateGUID() {
  var d0 = Math.random()*0x100000000>>>0;
  var d1 = Math.random()*0x100000000>>>0;
  var d2 = Math.random()*0x100000000>>>0;
  var d3 = Math.random()*0x100000000>>>0;
  return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+
    lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+
    lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+
    lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
}

(From: http://stackoverflow.com/a/21963136)

We also need to know how many products should be in the group once we're in the cart, so we can tell if any are missing. We'll pass both the GUID and the count of products into the cart as properties on each of the products. Here's a modified version of the previous article's addToCart method which handles those new properties.

var queue = [12345, 23456, 34567];
var groupCount = queue.length;
var groupID = generateGUID();
addToCart(queue, groupCount, groupID);
 
function addToCart(queue, groupCount, groupID){
  var productId = queue.shift();
 
  jQuery.post('/cart/add.js', {
    quantity: 1,
    id: productId,
    properties: {
      groupCount: groupCount,
      groupID: groupID
    }
  }, function() {
    if (queue.length) { addToCart(queue) };
  });
};

With the above method we've added 3 items to the cart, and each of them has a property with the same ID and has the count of how many products should be in the group. That information will need to be exposed in the cart to be operated on by functions which keep the products grouped together.

From Liquid to JavaScript

Products in the cart are usually displayed by iterating through them and outputting their attributes in Liquid, but we need to perform operations on these items to group them together based on their properties, and Liquid isn't the right tool for that job. Instead, if we can get this information into JavaScript objects we can control them much more flexibly. The below code would live in `cart.liquid` in your template.

<script type="text/javascript">
  // this will be our collection of all products in the cart
  window.cartItems = [];
 
  {% for item in cart.items %}
    var cartItem = {};
 
    // this assigns each of the products in our collection their properties, including
    // groupCount and groupID, so that we can create the correct groupings later on
    {% for property in item.properties %}
      {% assign property_first = property.first %}
      {% assign property_last = property.last %}
 
      cartItem['{{ property_first }}'] = '{{ property_last }}';
    {% endfor %}
 
    // we'll need the index if we want to manipulate this product later, like to change its
    // quantity or remove it from the cart
    cartItem.index = {{ forloop.index }};
 
    // additional information used to build the display of the item in the cart. we'll be
    // building the cart with JS instead of liquid, so we need to get this information into
    // the JS objects used for that process
    cartItem.title = "{{ item.product.title }}";
    cartItem.handle = '{{ item.product.handle }}';
    cartItem.productType = "{{ item.product.type }}";
    cartItem.price = '{{ item.line_price }}';
    cartItem.collectionUrl = '{{ collection_url }}';
    cartItem.imageUrl = '{{ item.product.featured_image | product_img_url: 'medium' }}';
    cartItem.imageAlt = "{{ item.product.featured_image.alt }}";
    cartItem.quantity = {{ item.quantity }};
 
    // we might want to list something multiple times if it's got a quantity greater than 1
    // to accurately describe the customized product
    for (var i = 0; i < cartItem.quantity; i++) {
      window.cartItems.push(cartItem);
    }
 
  {% endfor %}
</script>

The above code iterates through all items in the cart, collects their properties such as the count of products in the group and the groupID, as well as attributes such as their titles and prices which will be used to display the items in the cart. This info is necessary because we won't be displaying the products with liquid as is standard.

Building the Display

Now that we have the items collected and available to our script in `window.cartItems` we can begin to build the display of the cart. To do this we first need to collect information about the groups.

 

var groupedCartEntries = {};
 
function groupCartEntries(){
 
  for (var i = 0; i < cartItems.length; i++) {
    var item = cartItems[i];
    if(item.groupId){
      if(isEmpty(groupedCartEntries[item.groupId])){
        groupedCartEntries[item.groupId] = {
          groupCount: parseInt(item.groupCount),
          indices: []
        };
      }
 
      groupedCartEntries[item.groupId].indices.push(item.index);
    }
  }
}

This first function `groupCartEntries()` iterates through our cart items and stores where those items are in the cart (as far as Shopify is concerned) for later manipulation. It might result in an object which looks like this:

// groupedCartEntries
{
  "823279de-b235-45d4-8776-639fc0b8be28": {
    groupCount: 2,
    indices: [1, 2]
  }
}

Once our products are grouped, we can start to build our HTML from the info in the javascript objects.

function buildDisplay(){
  var groups = Object.keys(groupedCartEntries);
  var groupId, group;
 
  for (var k = 0; k < groups.length; k++) {
    groupId = groups[k];
    group = groupedCartEntries[groupId];
 
    buildGroup(groupId, group);
  }
}
 
function buildGroup(groupId, group){
  // first let's clone some HTML to use from a hidden exemplar
  // we can instead build it here from scratch if desired
  entryTemplate = $('.exemplar').clone();
  entryTemplate.removeClass('exemplar');
  entryTemplate.removeClass('hidden');
 
  var groupEntry = entryTemplate;
 
  // adding data attribute to a remove button so we can later remove the whole group at once
  groupEntry.find('._remover a').data('group-id', groupId);
 
  var infoList = groupEntry.find('._info').append('<ul></ul>').find('ul');
 
  var price = 0;
  for (var j = 0; j < group.indices.length; j++) {
    var index = group.indices[j];
    var item = cartItems[index];
 
    infoList.append('<li>' + item.title + '</li>');
    // get a total price for the group
    price += parseFloat(item.price);
  }
 
  groupEntry.find('._price').text(price);
 
  $('#grouped-cart-items').append(groupEntry);
}

Removing a Group

Now we've got a group of products on the page, and its got a removal button which references its groupID. We can use this to allow for removal of the group as a whole. GETting the `/cart/change` url with query string parameters allows us to make changes asynchronously, so we can be sure to delete all the products in the group before reloading the page.

function removeFromCart(groupId){
  var deleteQueue = groupedCartEntries[groupId].indices;
  processDeleteQueue(deleteQueue);
}
 
function processDeleteQueue(deleteQueue){
  var url = '/cart/change?line='+deleteQueue.pop()+'&quantity=0';
  $.get(url, function(data){
    if(!isEmpty(deleteQueue)) processDeleteQueue(deleteQueue);
    else {
      // reload page when done removing cart items
      window.location = '/cart';
    }
  });
}