Beyond Customized: Shopify and Javascript

By Dan Turner, Culture Foundry Developer / Thu Nov 30, 2017
The e-commerce vendor Shopify has a lot to offer, but its default method of product customization is too limited for many merchants.

At Culture Foundry we’ve developed a combination of methods to allow unlimited product varieties and product customization far beyond what’s built into the default templates.

The Limitations

The kinds of customization which Shopify offers by default include properties and product variants. Properties are key-value pairs which are passed along with the product when it’s added to the cart, and are purely informational; they don’t change the price of the product but do allow merchants to get information about the choices made by a customer which they can use when fulfilling the order. For example, properties might be used to collect information for engraving or embroidery:

  <div>
    <label for="monogram">Monogram</label>
    <input type="text" id="monogram" name="properties[Monogram]">
  </div>

(From Shopify Help Center)

Product variants allow for different pricing along with different shipping, inventory, weight, and fulfillment options, but are limited in to 100 total combinations of selections. For example, if those selections include size and color, you can have 10 of each and hit the limit, or 5 sizes and 20 colors, etc. With more independent product options, the limit is hit much more quickly.

For example, if you’re selling tee-shirts and they come in sizes S, M, L, XL, XXL, and are available in colors White, Black, Red, Blue, and Green, and you offer them in Men’s and Women’s styles, and with the option of No Logo, Logo on the Back, Logo on the Front, and Logo on both Front and Back, then you have 5 sizes, times 5 colors, times 2 styles, times 4 logo options, so your total variants are 5 * 5 * 2 * 4, which is 200, and double Shopify’s variant limit! What’s a merchant to do?

Adding Multiple Products to the Cart

Beyond simply providing a way for products to be grouped together for a custom product, adding multiple products to the cart allows for unlimited product variations. Items can be added along with the main product which have arbitrary weights and prices as needed to increase the cost or weight of the order. For example, one such ‘utility’ product might cost one penny, and as many as necessary can be used to increase the price of the product. Similarly, one might use a product which has no cost but has a weight of one ounce to increase the ship weight of the order. These additional products then can be hidden in the cart from the customer so this customization remains transparent.

When sending multiple products to the cart, a unique id and a count of items with that id can be added as properties to each. This allows the items to be grouped in the cart for display purposes and also to allows for scripting in the cart to maintain a consistent state, so if a customer attempts to remove one product in the group the entire group gets deleted from the cart. (Those techniques will be detailed in a future articles.)

Shopify provides a script cart/add.js, which can be POSTed to asynchronously to add several items to the cart from one click by the shopper. To add one item that looks like this:

jQuery.post('/cart/add.js', {
  quantity: 1,
  id: 794864229,
  properties: {
    'First name': 'Caroline'
  }
});

(example from Shopify Help Center)

To send multiple products to the cart a queue needs to be created and processed in JS, which would look like this:

var queue = [12345, 23456, 34567];
addToCart(queue);

function addToCart(queue){
  var productId = queue.shift();

  jQuery.post('/cart/add.js', {
    quantity: 1,
    id: productId,
  }, function() {
    if (queue.length) { addToCart(queue) };
  });  
};

In the above example, the queue contains several product IDs, which are sent to the addToCart function. That function takes the first ID out of the queue, sends it to the cart, and repeats until the queue is empty.

Additional failure handling can be done in the addToCart() function, so if the customization process is used to include additional parts with a product, and any of those parts are out of stock the cart can be cleaned up with a POST to /cart/update.js. [Note: examples from this point forward use “$“ instead of “jQuery“ as the jQuery variable name.]

var queue = [12345, 23456, 34567];
var sentProducts = [];
addToCart(queue, sentProducts);

function addToCart(queue, sentProducts){
  var productId = queue.shift();

  $.post(
    '/cart/add.js',
    {
      quantity: 1,
      id: productId,
    },
    null,
    'json'
  ).success(function(){
    sentProducts.push(productId);
    if(queue.length){
      addToCart(queue, sentProducts);
    } else {
      window.location = '/cart';
    }
  }).error(function(e){
    removeFromCart(sentProducts);
  });
}

function removeFromCart(sentProducts){
  if(!sentProducts.length) return;

  /* this naively sets the quantity to zero, but if the customer has more of these items in the cart and we want to preserve them we can first get data about what’s in the cart with a call to /cart.js */

  var removalData = {updates: {}};
  for (var i = 0; i < sentProducts.length; i++) {
    removalData.updates[sentProducts[i]] = 0;
  }

  $.post(
    '/cart/update.js',
    removalData,
    null,
    'json'
  ).success(function(){
    // handle success
  });
}

In the above example if there is an error adding an item to the cart the cart is then updated to set the quantity for all of the items originally in the queue to zero.

Many, Many Parts

With the above techniques, we can add multiple items to the cart at once, but how should we get the product information for those items? Let’s say there are several hundred products we want to allow to be combined. We can make information about them available to JavaScript with some clever liquid templating, like so:

var parts = [
  {% for product in collections['addon-parts'].products %}
    {{ product.id }},
  {% endfor %}
];

We can add more data, also, to create our select lists for those part:

var parts = [
  {% for product in collections['addon-parts'].products %}
    {
      id: {{ product.id }},
      title: {{ product.title }},
      price: {{ product.price }},
    },
  {% endfor %}
];

for (var i = 0; i < parts.length; i++) {
  option = '' + parts[i].title + '($' + parts[i].price +')';
  $('select').append(option);
}

But what if there are hundreds of parts? That might end up being a pretty weighty page! Also, Shopify limits products from a collection to 1000, so if you’ve got more than 1000 products in the collection from which you want to allow selection you’ll need a different approach.

A Different Approach

We can save page weight and avoid collection limitations by getting this product data asynchronously rather than including the JSON on the product page itself. This has the advantage of allowing the visitor’s browser to cache the retrieved data so if it’s used on multiple pages they’ll only be downloading it once. To make this work, we’ll need a new Page, and we’ll assign it a custom template. Here’s a simple example template:

{"products":
  [
    {% paginate collections['addon-parts'].products by 1000 %}
      {% for product in collections['addon-parts'] %}
        {{ product | json }}{% unless forloop.last %},{% endunless %}
      {% endfor %}
    {% endpaginate %}
  ],
  "nextURL": {{ paginate.next.url | json }}
}

Note {% unless forloop.last %},{% endunless %} has been added next to our product JSON output. Without this conditional, we won’t be creating parsable JSON.

To get this data into JavaScript we’ll need to keep getting data as long as there are more pages.

var nextURL = "/pages/addon-parts";
var products = [];
getProducts(nextURL);

function getProducts(nextURL){
  $.ajax({
    url: nextURL,
    dataType: 'json',
    success: function(data){
      Array.prototype.push.apply(products, data.products);

      if(data.nextURL){
        getProducts(data.nextURL);
      } else {
        // do something with those products, like create options in select dropdowns.
      }
    }
  });
}

One more technique you’ll need to use to make this work more reliably is that the data coming in from getting this URL is going to be wrapped by your layout/theme.liquid, so it’s not valid JSON. You can make an exception for this page in your layout/theme.liquid by checking the page name, like this:

{% if page.handle == 'addon-parts' %}{{page.content}}{% else %}......

Shopify adds an admin bar to the HTML the server if you’re logged in as an admin, though, so we need something a bit more robust to keep the output parsable:

  {"products":
    [
      {% paginate collections['addon-parts'].products by 1000 %}
        {% assign item_count = 0 %}
        {% for product in collections['addon-parts'] %}
          {{ product | json }}{% unless forloop.last %},{% endunless %}
        {% endfor %}
      {% endpaginate %}
    ],
    "nextURL": {{ paginate.next.url | json }}
  }

  <script type="text/javascript">
    var nextURL = "/pages/addon-parts";
    var products = [];
    getProducts(nextURL);

    function getProducts(nextURL){
      $.ajax({
        url: nextURL,
        success: function(data){
          var start = data.indexOf('');
          var end = data.indexOf('');
          var cleanedData = data.slice(start + ''.length, end);
          var json = JSON.parse(cleanedData);

          Array.prototype.push.apply(products, json.products);

          if(json.nextURL){
            getProducts(json.nextURL);
          } else {
            // do something with those products, like create options dropdowns.
          }
        }
      });
    }
  </script>

This is Part one of a two part article. Click here for Part 2: Beyond Customized 2: Shopify and Javascript