Multiple attributes without variants in Shopify

As promised over there, I will present you with the algorithm and the JavaScript code needed to implement multiple attributes for products in Shopify without variants. With this method, you are not squandering any of your precious SKUs.

You will present your visitors with one drop-down select box per attribute your product has (for example, one for size, and one for color...). The method works for an unlimited number of attributes, ie. two, three, whatever. The chosen options for the cart items attributes will be stored in a web cookie and submitted to Shopify's 'back-end' during the checkout process. This method uses JavaScript.

A shop has been created to which the modifications presented in this tutorial have been applied. The theme is based on the sea version of the Vogue theme by Jared Burns. The website is here. Feel free to visit the 11heavens Bags website and view the source.

A store that sells bags

Let's start with a context, as a context makes the whole process fun. You want to sell bags and you signed up for a Basic Plan. That plan gives you 100 SKUs. 100 SKUs means 100 variants. That's a little restrictive for you as you have 80 bags to sell, but that'll do. However, each of your bags come in 3 different types of 'material' and 4 different colors, so what to do?

Color:

  1. Blue
  2. Black
  3. Yellow
  4. Shit brown

Material:

  1. Degradable within a week
  2. Plastic
  3. Faux leather

Without resorting to JavaScript here, you would require 80 [products] x 4 [types of material] x 3 [types of color] = 960 SKUs or variants. And your customers would have to pick among 12 variants for each product: blue degradable, blue plastic, blue faux leather, black degradable, black plastic, black faux leather, yellow degradable, yellow plastic, yellow faux leather, brown degradable, brown plastic, brown faux leather. Picking from a drop-down select element comprised of 12 options is not exactly lovely. Seeing these 12 options in a table next to 12 radio buttons is just as crappy.

Any combination of the above (say, blue plastic) costs the same. If any attribute affects your selling price than you must create a variant for it. Then, you may not use a JavaScript attribute.

Waiting for Shopify to fix its bugs in the Bulk Import for products, you've only created two products in your Shopify dashboard, an awful bag, and a doggy bag.

For now, each product has one 'Default' variant, which you have renamed to Bag. Because you do not like the word default.

Your product page showing one variant

Using the jQuery library

I will be using the jQuery library in this tutorial, but you should use any JavaScript library you know well. Work with what you know and love. If you don't really know any library, I strongly recommend using jQuery because it is easy to learn and light-weight. The latest release of jQuery on this day (August 28th, 2008) is 1.2.6. But go ahead and download the latest release, whichever 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 an other 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 use the jQuery object.

{{ '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. And then you can write jQuery code in any template without conflict. Of course, you will have to leave the $ alias alone! Unless you write code to be understood by another library that makes use of the alias. For the full, technical explanation, head over here.

We are now ready to implement our JavaScript solution. But first, we'll write XHTML to add two drop-down select elements to the Add to Cart form in our product template.

Working on the product pages

There shouldn't be a radio button next to that single variant

The Vogue theme (the one I am basing this tutorial on) is good to go for several variants. It presents a list of options on the product page, to buy either variant 1 or variant 2 etc. of a product. The problem with that XHTML is that you are still presented with a radio button when only one variant of the product is available for purchase, as is the case here. If there is no option for you to choose from, there ought to be no radio button to tick. We will clean that up.

Open your product.liquid template and edit the form element like so:

<form action="/cart/add" method="post">
  <div id="product-variants">{% assign variant = product.variants.first %}
    <p class="odd">
      <input type="hidden" name="id" value="{{ variant.id }}" id="hidden_{{ variant.id }}" />
      <label for="hidden_{{ variant.id }}">
        <span class="bold-blue">{{ variant.price | money }}</span>
        {% if variant.price < variant.compare_at_price %}
        <del>{{ variant.compare_at_price | money }}</del>
        {% endif %}
      </label>
    </p>
  </div>
  <input type="image" src="{{ 'purchase.png' | asset_url }}" name="add" value="Purchase" id="purchase" />
</form>

Basically, what we've set up to do here is to examine only the first variant of our product and assign that variant to a variable called variant.

{% assign variant = product.variants.first %}

Then we changed the type of the input element from radio to hidden. That way, the variant id still gets submitted with the form — and hence the variant is added to the cart — but no radio button is shown anymore.

<input type="hidden" name="id" value="{{ variant.id }}" id="hidden_{{ variant.id }}" />

Furthermore, we got rid of the check on the availability of our variant. The thing is, if the product is available in only one variant (as it is here), and that variant is out of stock, Shopify is smart enough to set the value of product.available to false, hence the form is not even output to screen (refer yourself to your product.template to convince you of this.)

Why use a list element if there's only one item in it? No reason, so we got rid of that too. However, we had to modify our CSS a tiny bit to make the whole thing still look pretty:

#product-variants {
  width: 13em;
  font-size: 125%; 
}
 
#product-variants p.odd {
  padding: 4px 6px;  
}

Our hidden field label is displayed, so what we end up with now is no radio button next to the set price. Super.

Your product page showing one variant without a radio button

Adding our SELECT elements to the product template

Time to add these drop-down select widgets to account for our product attributes. We'll write the markup for two select elements, one for the material attribute and another for the color attribute. Time to brush up on your XHTML skills.

<select name="material">
  <option value="Degradable within a week" selected="selected">Degradable within a week</option>
  <option value="Plastic">Plastic</option>
  <option value="Faux leather">Faux leather</option>
</select>
<select name="color">
  <option value="Blue" selected="selected">Blue</option>
  <option value="Black">Black</option>
  <option value="Yellow">Yellow</option>
  <option value="Shit brown">Shit brown</option>
</select>

To provide this:

About the value and selected attributes

For the value attribute of each option, use human-readable text. Do not use text that is handleized, or camelCased — or what not. You will display theses values in the cart later on.

Move the selected="selected" attribute to whichever option element makes more sense as a default. If plastic black bags are your most popular, then rewrite your SELECT elements like so:

<select name="material">
  <option value="Degradable within a week">Degradable within a week</option>
  <option value="Plastic" selected="selected">Plastic</option>
  <option value="Faux leather">Faux leather</option>
</select>
<select name="color">
  <option value="Blue">Blue</option>
  <option value="Black" selected="selected">Black</option>
  <option value="Yellow">Yellow</option>
  <option value="Shit brown">Shit brown</option>
</select>

To provide this:

This markup can be placed within the form element or not. What I mean here is that this XHTML can be copied and pasted between the opening and closing tags of the form element, or pasted outside of it. The selected options will NOT get submitted with the form. The selected options will be stored in a cookie upon form submission, and that is different.

Let's place the select elements inside the form, and add text above and below these elements — to explain to the visitor what to do about them.

<form action="/cart/add" method="post">
  <div id="product-variants">{% assign variant = product.variants.first %}
    <p class="odd">
      <input type="hidden" name="id" value="{{ variant.id }}" id="hidden_{{ variant.id }}" />
      <label for="hidden_{{ variant.id }}">
        <span class="bold-blue">{{ variant.price | money }}</span>
        {% if variant.price < variant.compare_at_price %}
        <del>{{ variant.compare_at_price | money }}</del>
        {% endif %}
       </label>
     </p>
   </div>
   <p>Select a material and color for your {{ product.title }}:</p>
   <select name="material">
     <option value="Degradable within a week">Degradable within a week</option>
     <option value="Plastic" selected="selected">Plastic</option>
     <option value="Faux leather">Faux leather</option>
   </select>
   <select name="color">
     <option value="Blue">Blue</option>
     <option value="Black" selected="selected">Black</option>
     <option value="Yellow">Yellow</option>
     <option value="Shit brown">Shit brown</option>
   </select>
   <p class="no-javascript">If JavaScript is disabled in your browser,
     please write down the type of material and color you want for your bag
     in the 'special instructions' box on the
     {{ 'cart page' | link_to: '/cart' }} before checkout.</p>
  <input type="image" src="{{ 'purchase.png' | asset_url }}" name="add" value="Purchase" id="purchase" />
</form>

Note that we'll hide that special note added at the bottom using JavaScript so that visitors who do have JavaScript enabled in their browser won't see it. The special instructions text box on the cart page will provide an alternative for those who browse without JavaScript, an alternative to inform us, the shop owner, about the material and color they want for their bags.

We will style our select elements up a little bit, simply because we do not like things that look untidy (I am a total clean freak). Let's add space below and make both elements equally large. Since that purchase button is by default inline, it will tend to crop up next to our select elements so let's make it behave like a block element.

select {
  margin-bottom: 10px;
  width: 15em;
}
#purchase {
  display: block;
}

Hiding some text from people who browse with JavaScript

We will write some JavaScript to hide the text added just previously from people who do browse with JavaScript. These people don't need to be told to add special instructions to their cart. We will use a hook to access that special paragraph and then simply hide it using the jQuery method hide(). We have to hide that paragraph when the document is 'ready' so we have to wrap our code as shown below. Open your theme.liquid template and add this:

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
<script type="text/javascript">
  jQuery.noConflict();
  jQuery(function() {
  /* some code here to execute when the document is ready */
  });
</script>

The jQuery we need to use is quite simple and of the form jQuery('.className').hide(). We will select our paragraph based on its class name.

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

To test this, disable JavaScript in your browser (I use the Web Developer Toolbar for this), refresh the web page and see the special instruction for visitors who browse without JavaScript.

Your product page showing one variant without a radio button, your attributes and special instructions.

Re-enable JavaScript, refresh, and see the special instruction disappear:

Your product page showing one variant without a radio button, your attributes without special instruction.

Storing attributes in a cookie using JavaScript

I said I'd share an algorithm with you. I will now provide it in a nutshell. We will store our attributes in a cookie when the visitor clicks on the Purchase button. Once our visitor lands on the cart page, we will read from our home-baked cookie and do two things:

  1. Display the options chosen next to the cart item, as a 'here is what you had picked earlier on' reminder, and
  2. Submit the options chosen with the checkout form using properly named form fields — named so that Shopify will pick up the information and display it in the transaction slip.
Head First JavaScript on cookies: Cookies are like variables that get stored on the user's hard drive by the browser so that they last beyond a single web session. In other words, you can leave a page and come back, and the data's still there.

This poses 2 difficulties:

  1. We want to write to our web cookie the attribute options chosen possibly for quite a few items, as the cart may contain several items.
  2. A web cookie can only store text, while what we're trying to keep track of is a relatively complex data structure.

CART ITEM 1 (Doggy Bag)
+ material: faux-leather,
+ color: shit-brown
CART ITEM 2: (Awful Bag)
+ material: plastic,
+ color: black

In this tutorial, we will use JSON to keep track of our cart items' attributes. JSON is just text, so JSON can be stored in a web cookie. We're lucky because JavaScript provides tools to translate any complex data structure to JSON, and to translate that JSON back to the original data structure.

From Wikipedia: JSON (pronounced /ˈdʒeɪsɒn/, i.e., "Jason"), short for JavaScript Object Notation, is a lightweight computer data interchange format. It is a text-based, human-readable format for representing simple data structures and associative arrays (called objects).

To translate to JSON and back, we will use a jQuery plugin. Download the minified version of the plugin and upload this file to your Assets directory — the same way you added the jQuery library previously. Then, link to it in your theme.liquid file:

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

Although using web cookies is no rocket science, it can still be a little quirky, so we'll use a jQuery plugin to read and write to our web cookie. Download the cookie plugin, upload it to your Assets directory, and link to it.

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

Why are we using so many plugins here? Out of the box, due to its light-weight nature, jQuery provides remarkable tools for selecting DOM elements, event-handling, DOM scripting and AJAX. And that's about it.

Let's try something simple, as an exercise. Upon submission of the Add to Cart form, let's pop up an alert message telling us how much items are in the cart. We will use the following jQuery code to achieve this:

/* When user clicks on any element with id set to 'purchase'... */
jQuery('#purchase').click(function() {
  alert({{ cart.item_count }});
});

We are here listening to a click event associated to all elements with an id attribute set to 'purchase'. As it turns out, our purchase button has such id. We're telling the browser to pop up an alert message when such event occurs.

So we will amend our theme.liquid head element like so:

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
{{ 'jquery.json.min.js' | asset_url | script_tag }}
{{ 'jquery.cookie.js' | asset_url | script_tag }}
<script type="text/javascript">
  jQuery.noConflict();
  jQuery(function() {
    jQuery('.no-javascript').hide();
    jQuery('#purchase').click(function() {
      alert({{ cart.item_count }});
    });
  });
</script>

To see the alert box, click on the Purchase button.

Alert box where there's no item in the cart

Let's play with cookies now.

Let's begin by baptizing our cookie. We'll name it shopify_cart_attributes. Avoid space and capital letters here. The way we'll play with our web cookie will be as trivial as this:

var myJSON = jQuery.cookie('shopify_cart_attributes'); // read from the cookie
jQuery.cookie('shopify_cart_attributes', myJSON, configuration); // write to the cookie
jQuery.cookie('shopify_cart_attributes', null, configuration); // delete the cookie

So we will amend our theme.liquid head element like so:

{{ 'jquery-1.2.6.min.js' | asset_url | script_tag }}
{{ 'jquery.json.min.js' | asset_url | script_tag }}
{{ 'jquery.cookie.js' | asset_url | script_tag }}
<script type="text/javascript">
  jQuery.noConflict();
  jQuery(function() {
    jQuery('.no-javascript').hide();
    jQuery('#purchase').click(function() {
      var myValue = 'Hello beautiful!';
      var configuration = {expires: 14, path: '/', domain: '{{ shop.domain }}'};
      jQuery.cookie('shopify_cart_attributes', myValue, configuration);
      alert(jQuery.cookie('shopify_cart_attributes'));
    });
  });
</script>

Alert box to tell you that you are beautiful...

Something important to understand at this point. When a visitor adds a new product to his shopping cart, chances are his cart is not empty. If the cart already contains stuff, then it means the cookie is already set, exists. We must not overwrite its value without first storing its current value. If we simply overwrite it, it would be disastrous, we would lose the attributes set for all the other cart items. Instead, we must only add information to our cookie. The way we go about this is to first check if the cookie exists. If it doesn't exist, then the value returned by reading the cookie will be null. If the cookie does exist, we will store its JSON value to a variable, translate this variable, modify the result, and then translate the whole thing back to JSON and store it to the cookie again, effectively overwriting it, but this time with an up to date value.

Using jQuery, we will listen to the submit event on the Add to Cart form. To get a hold of the form element that submits an item to the cart, we will open our product.liquid template and add an id attribute to the form. Like so:

<form action="/cart/add" method="post" id="add-to-cart">

Then, we will open theme.liquid again.

Read the comments. They are in green. There are more comments than there is code.

jQuery.noConflict();
jQuery(function() {
  jQuery('.no-javascript').hide();
  // console.log('Value of the cookie is %s', jQuery.cookie('shopify_cart_attributes'));
  // Listening to the submit event on the form with id 'add-to-cart'
  jQuery('#add-to-cart').submit(function() {
    // Prepare an Array to store the attributes for each cart item.
    var items = [];    
    // Reading the cookie.
    var myJSON = jQuery.cookie('shopify_cart_attributes');    
    // If the cookie does exist.
    if (myJSON != null) {
      // Parse the cookie.
      items = jQuery.evalJSON(myJSON);
    }
    // Creating an attribute object.
    var attribute = {};
    // Assigning values to its properties.
    // The material chosen, the color, the variant ID and some unique ID.
    // The variant ID and the unique ID is to help us keep track of what is what.
    attribute.material = jQuery('select[name=material]').val();
    attribute.color = jQuery('select[name=color]').val();
    attribute.variant_id = '{{ product.variants.first.id }}';
    attribute.unique_id = '{{ 'now' | date: '%H%M%S' }}';
    // Adding the attribute object to the items array.
    items.push(attribute);
    // Writing to the cookie.
    var configuration = {expires: 14, path: '/', domain: '{{ shop.domain }}'};
    jQuery.cookie('shopify_cart_attributes', jQuery.toJSON(items), configuration);
    // console.log('Value of the cookie is %s', jQuery.cookie('shopify_cart_attributes'));
    // Submitting the form to add the item to the cart.
    return true;
  });
});

The Liquid Tag {{ shop.domain }} provides you with the domain name of your shop. The web cookie that we're creating and using here is a path cookie. It has the form Set-Cookie: name=value; expires=date; path=/; domain=.example.org. The cart cookie Shopify creates from its back-end is also a path cookie.

Why the unique ID

Several cart items with differing attributes can share the same variant ID. Although it will certainly be useful to keep a record of the variant ID of the item added to the cart, it will also be useful to tag each cart item in our cookie with a unique identifier, called unique_id. We could use the Liquid Tag {{ cart.item_count }} to compute such unique ID, as this Tag is incremented automatically when an item is added to the cart. However, if the user later edits his cart on the cart page, and leaves the cart page to add yet another item, he may end up with 2 cart items sharing the same ID. There are 2 ways to generate a unique ID in computer programs: use a global variable that is incremented and never reset, or use a timestamp. I have decided to use a timestamp, and I use Liquid to generate it:

// @see http://wiki.shopify.com/FilterReference#date
attribute.unique_id = "{{ 'now' | date: "%H%M%S" }}";

Uncomment the console.log instructions in your JavaScript code and view your site in Firefox. Using the Firebug console, you will be able to read the cookie value before and after you click on the Purchase button. You can use the Web Developer Toolbar add-on to delete all your path cookies. This will effectively delete Shopify's cart cookie and our own cookie.

If I add a blue plastic bag and a faux leather yellow bag to my cart, the shopify_cart_attributes cookie as shown in Firebug will become:

[{"material": "Plastic", "color": "Blue", "variant_id": "7057372", "unique_id": "033206"}, {"material": "Faux leather", "color": "Yellow", "variant_id": "7057372", "unique_id": "033239"}]

We will soon turn our attention to the cart page, but first we will clean things up a little bit.

Where to put JavaScript and graceful degradation

You may have heard of unobtrusive JavaScript and graceful degradation and the ideal of placing all JavaScript code in the head element of the document, or even better in a separate file. You may also have heard of namespacing functions and variables — avoiding defining functions and variables in the global-scope, in other words. The Vogue theme is an example of having done everything the wrong way, when it comes to JavaScript best practices, you could say, yet it works. In our own code, we will try and live up to a few standards.

Unobtrusive JavaScript means that we should separate behavior from markup, hence avoid adding event handlers in the code. This HTML/Liquid from Vogue's cart.liquid is bad, because an onclick attribute is used to execute JavaScript right in the HTML.

<td class="basket-column"><a href="#" onclick="remove_item({{ item.variant.id }}); return false;">Remove</a></td>

A function is also declared in the midst of the cart.liquid page, outside of the head element, the function remove_item(), and it is declared without a namespace, so it is a global scope function. Bad.

We will not correct the Vogue's implementation of JavaScript, that certainly ain't our goal. However, we will try and make the JavaScript we write — our own JavaScript — more classy.

We are not just being anal. The ideal of behavior and markup separation is handy. You can place all your code in one place — and not look for it all the time (to make modifications to it). And the separation helps with graceful degradation. Speaking of which, if JavaScript is disabled for the visitor, we're still showing him the select boxes for attributes, and we're presenting him with a message asking him to type his chosen attributes in a text box on the cart page. So you could say that we are trying to make the shop still usable without JavaScript. People that browse without JavaScript can still buy a blue plastic bag, provided that they know that a blue plastic bag is available — and they will — and they can inform us, the buyer, of their preferences, on the cart page, if they are clearly instructed to do so, and they are.

All JavaScript code we wrote so far has been placed in the head element of theme.liquid.

The template theme.liquid is our XHMTL document placeholder. However, the code we wrote was only needed on product pages. Although it is far from necessary, we could wrap our JavaScript code in a Liquid If Tag, like so:

// console.log('Value of the cookie is %s', jQuery.cookie('shopify_cart_attributes'));
{% if template == 'product' %}
// If we are on a product page.
jQuery(function() {
  jQuery('.no-javascript').hide();
  // Listening to the submit event on the Add to Cart form.
  jQuery('#add-to-cart').submit(function() {
    // Prepare an Array to store the attributes for each cart item.
    var items = [];    
    // Reading the cookie.
    var myJSON = jQuery.cookie('shopify_cart_attributes');    
    // If the cookie does exist.
    if (myJSON != null) {
      // Parse the cookie.
      items = jQuery.evalJSON(myJSON);
    }
    // Creating an attribute object.
    var attribute = {};
    // Assigning values to its properties.
    // The material chosen, the color, and the variant ID.
    // The variant ID and the unique ID is to help us keep track of what is what.
    attribute.material = jQuery('select[name=material]').val();
    attribute.color = jQuery('select[name=color]').val();
    attribute.variant_id = '{{ product.variants.first.id }}';
    attribute.unique_id = '{{ 'now' | date: '%H%M%S' }}';
    // Adding the attribute object to the items array.
    items.push(attribute);
    // Writing to the cookie.
    var configuration = {expires: 14, path: '/', domain: '{{ shop.domain }}'};
    jQuery.cookie('shopify_cart_attributes', jQuery.toJSON(items), configuration);
    // console.log('Value of the cookie is %s', jQuery.cookie('shopify_cart_attributes'));
    // Submitting the form to add the item to cart.
    return true;
  });
});
{% endif %}

All I did here was put the JavaScript code between Liquid tags:

{% if template == 'product' %}
/* JavaScript code we need only on product pages. */
{% endif %}

Do this, then do a View Source on any page that is not a product page, you will not see the JavaScript code we wrote (minus the commented console.log instruction), our code will not have been output to the browser. Then go to a product page, and there it is, voilà. Using these Liquid Tags makes the source prettier, cleaner, there is less JavaScript to parse for the browser. This is also to prepare you... for when JavaScript MUST be executed ONLY on certain pages, in certain conditions that Liquid can inform us about.

Working on the cart page

Now that we've covered what needed to be done on product pages, we will move our attention to the cart page.

Setting up a list of tasks for the cart page

Once our visitor lands on the cart page, we will read his web cookie and accomplish, at least, two things:

We will need to make room on the cart page to display the chosen attributes for each cart item.

The way Shopify organizes the shopping cart is by product variant. Each row in the cart table provides information about cart items (plural or singular) that are of the same product variant garden variety. In other words, a LineItem (as Shopify calls it) is a subset of the cart, and contains information about items that have the same SKU. In our case, we can further differentiate between the items that are part of the LineItem, because some items in there may have different attributes (different color, and different material).

From the Shopify Wiki: A line item represents a single line in the shopping cart. There is one line item for each distinct product variant being purchased, regardless of the quantity of that variant in the cart.

Creating an extra column in the cart table to display attributes

We will now add a new column to the cart table. For this, we will need to add HTML elements to our cart.liquid template, one HTML element to add a header for the column, and an other to add the LineItem cell for that new column. Open your cart.liquid template and locate your HTML table element.

We will add a column between the Description and Price columns and name it Items.

...
<th>Description</th>
<th>Items</th>
<th>Price</th>
...

Then we will add a td element between the Description and Price cells and fill it with a paragraph, that, in turns, will contain a form input of type hidden.

<td class="basket-column-one">
...
</td>
<td class="basket-column basket-column-two">
<p id="{{ item.variant.id }}">
  <input type="hidden" 
  name="attributes[{{ item.product.handle }}-{{ item.variant.title | handleize }}]" 
  value="" />
</p>
</td>
<td class="basket-column">
...
</td>

The class name basket-column-two will help us with CSS styling later on — we're also using the default class name basket-column.

Let's examine the content of that data cell.

<td class="basket-column basket-column-two">
<p id="{{ item.variant.id }}">
  <input type="hidden" 
  name="attributes[{{ item.product.handle }}-{{ item.variant.title | handleize }}]" 
  value="" />
</p>
</td>

We are sending with our checkout form extra fields named attributes[SOME_LINE_ITEM_IDENTIFIER]. The square brackets mean that we're sending an array. Indeed, each LineItem will have its own input element with name attributes[SOME_LINE_ITEM_IDENTIFIER]. The identifier must inform us, the buyer, about the LineItem, what it is. The LineItem ID (line_item.id) is just a number and not very telling for us. So I have chosen here to send an identifier that includes both the product handle and the variant title (turned into a handle, handleized). The combination will be unique, of course, as it is unique for each LineItem. The value associated with each field will tell us in the transaction slip about the attributes of all cart items that belong to that product variant. The name attributes has not been picked arbitrarily. We have to name these input fields attributes.

From the Shopify Wiki: The cart.attributes property is an optional field you can add to your shop’s checkout form. Simply define an input field of any type and name it “attributes[]” and it will be captured automatically and displayed on the order detail page in your admin area.

Reading the cookie and filling up the Items column with sets of attributes

Time to write some JavaScript code. Reading the cookie and parsing it should be familiar to you by now. Open your theme.liquid template and add a Liquid wrapper between your script tags.

{% if template == 'cart' and cart.item_count > 0 %}
// If we are on the cart page and the cart is not empty.
{% endif %}

Our JavaScript code will be sent to the browser and parsed by it only for the cart page, and when the cart is not empty.

The code now.

{% if template == 'cart' and cart.item_count > 0 %}
// If we are on the cart page and the cart is not empty.
// When the DOM is ready.
jQuery(function() {  
  // Reading the attributes from the cookie for display.
  var myJSON = jQuery.cookie('shopify_cart_attributes');
  var items = jQuery.evalJSON(myJSON);
  // Iterating through the cart items.
  for (var i=0; i<items.length; i++) {
    var attribute = 'Material: ' + items[i].material + ', Color: ' + items[i].color;
    // Creating a span element and filling it up.
    var item = jQuery('<span></span>').text(attribute).attr('id', items[i].unique_id);
    // Appending the span element to the DOM.
    jQuery('#' + items[i].variant_id).append(item).append('<br />');    
  }
});
{% endif %}

And we're done with at least displaying the attributes for each cart item on the cart page. We will add to this code later on. For example, we will add a 'remove' link next to each cart item set of attributes, so that a cart item with specific attributes can be removed from the cart — as opposed to removing all cart items that belong to the same product variant.

The DOM manipulation in the last snippet of code is a little complex. In a nutshell, for each cart item, we are creating a brand-spanking new span element, and filling it up with the attributes description, read from the cookie. We are giving an id to that span, and it is the unique id we had computed earlier on for the cart item in the cookie. Then we're looking for the LineItem paragraph element that corresponds to the variant ID of our cart item and append our new span element to it. The br element is added so that each set of attributes appears on its own separate line. For readability, in other words.

Removing the ability to edit the Qty of cart items

Something doesn't make sense here: a user can update the number of cart items of the same product variant. How can we keep track of the attributes of the cart item that is deleted or added this way? That is crazy talk. So we'll remove the ability for the shopper to change the LineItem quantities. We will keep that field, as Shopify requires it, but we will change its type from text to hidden. Changing a form field's type from text to hidden removes the ability for the user to edit it. We could simply disable the text field, but the user would wander why he's not able to edit it (what is going on, here, will he think, what have I done wrong?).

Open you cart.liquid template and look for this HTML:

<td class="basket-column"><input type="text" size="4" name="updates[{{item.variant.id}}]" id="updates_{{ item.variant.id }}" value="{{ item.quantity }}" onfocus="this.select();"/></td>

Edit it to:

<td class="basket-column"><input type="hidden" name="updates[{{item.variant.id}}]" id="updates_{{ item.variant.id }}" value="{{ item.quantity }}" />{{ item.quantity }}</td>

We will also hide the Update button, using CSS. Speaking of CSS, we will also style our attributes sets in the table.

#basket td.basket-column-one .basket-images {
  margin-left: 0;  
}
 
#basket td.basket-column-one .basket-desc {
  width: inherit;
}
 
#basket td.basket-column-two {
  width: 34em;
}
 
#update-cart {
  display: none;
}

Your cart page without an update button

Submitting the attributes info with the cart

While we implement this part, we need to test it using a Bogus Gateway. Shopify provides one. You can set it up on your admin/preferences/payment page. The instructions on how to setup the Bogus Gateway would push this tutorial length over the edge.

But the following screen capture should provide a strong hint:

Your Checkout & Payment page

Open you theme.liquid template. Time to amend our cart JavaScript code.

{% if template == 'cart' and cart.item_count > 0 %}
// If we are on the cart page and the cart is not empty.
jQuery(function() {
  // If the DOM is ready.
  // Reading the attributes from the cookie for display AND submission.
  var myJSON = jQuery.cookie('shopify_cart_attributes');
  var items = jQuery.evalJSON(myJSON);
  // Re-setting the value of our hidden attributes[] fields to empty strings.
  jQuery('td.basket-column-two input[type=hidden]').val('');
  // Iterating through the cart items.
  for (var i=0; i<items.length; i++) {
    var var_id = items[i].variant_id;
    var attribute = 'Material: ' + items[i].material + ', Color: ' + items[i].color;
    var item = jQuery('<span></span>').text(attribute).attr('id', items[i].unique_id);
    jQuery('#'+ var_id).append(item).append('<br />'); 
    // Updating our attributes[] fields.
    attribute = jQuery('#'+ var_id +' input').val() +' ['+ attribute +']';
    jQuery('#'+ var_id +' input').val(attribute);
  }
});
{% endif %}

We added some JavaScript code to the for loop that iterates through the parsed cookie. Not only are we displaying the attributes of our cart item in the cart table, but we are now appending the attributes info to the corresponding LineItem hidden attributes[] field. We're surrounding the attributes set with brackets:

attribute = jQuery('#'+ items[i].variant_id +' input').val() +' ['+ attribute +']';

We could have used parenthesis, or whatever your whims dictate in this situation. The attributes[] value is for your eyes only. The value will be displayed on the order detail page in your admin area. And you need a bogus gateway to see it, to make sure that it does appear there.

In jQuery, depending on how it's used, val() is either a getter or a setter function. If we call it with a parameter, we are setting the value of a form field. If we call it like this: val(), without a parameter, we are reading the value of a form field. You will notice in the above code that we're reading the value of the attributes[] field, storing it, appending to it and rewriting the field's former value with an updated one. We do this because that field may contain information about more than one cart item. We do not want to overwrite the field's value but add to it, while we iterate through our parsed cookie.

// Updating our attributes[] fields.
attribute = jQuery('#'+ var_id +' input').val() +' ['+ attribute +']';
jQuery('#'+ var_id +' input').val(attribute);

If you have trouble reading the above code, read it broken down like so:

// Updating our attributes[] fields.
var oldAttribute = jQuery('#'+ var_id +' input').val();
var newAttribute = oldAttribute +' ['+ attribute +']';
attribute = newAttribute;
jQuery('#'+ var_id +' input').val(attribute);

And since some browsers memorize the values of form fields, we need to re-set our form fields to empty strings whenever a user lands on the cart page — before we iterate through the parsed cookie.

// Re-setting the value of our hidden attributes[] fields to empty strings.
jQuery('td.basket-column-two input[type=hidden]').val('');

Go through the checkout process. Add a few items to your cart. On the checkout page, use '1' as credit card number, and '123' as card NIP. Do not forget to change the expiration month, which defaults to January of this year.

The checkout page

You can also abandon the order half way, after filling up page 1. You will be able to read the attributes in your abandoned order slip.

Checking up attributes in your order

Shopify deletes its own cart cookie, and so should we

Shopify deletes its own cart cookie when a transaction is completed. A transaction that is abandoned during the checkout process is not a completed transaction. If you go through a transaction yourself, as a buyer, using the Bogus Gateway, and you do not go through the process till the end, that is, until your bogus credit card is charged, then your cart will not be emptied upon your return in the shop. If you do go through the process, the transaction is marked completed and the cart is emptied.

How do we keep track of this ourselves? There is a very simple trick, a quite clever one, I am quite pleased to have come up with it on my own (like everything else here, for the matter). Ok... I am bragging. So what's the trick? Shopify, in its glorious magnanimity, always keeps its cart.item_count variable up to date. When a transaction is completed, Shopify deletes its cart cookie and resets the cart item count to zero. The trick involves adding these fews lines of code to your JavaScript, in theme.liquid:

{% if template == 'cart' and cart.item_count == 0 %}
// Deleting our cookie.
var configuration = {expires: 14, path: '/', domain: '{{ shop.domain }}'};
jQuery.cookie('shopify_cart_attributes', null, configuration);
{% endif %}

Using this trick, we will no doubt delete the cookie even when it doesn't exist. We will attempt to delete a non-existing cookie. But no harm will be done, no error thrown at us, if we do so. We could check if the cookie exists, but it is not necessary, it would be verbose and just add lines of code, and I love minimalism.

Removing cart items of a product variant

The Vogue theme provides a handy Remove link for each LineItem in the cart table. This works for us. However, there is a small problem. We need to update our cookie when a bunch of items are removed from the cart in this fashion. We will amend the Vogue's theme JavaScript to keep our cookie in sync.

Open your cart.liquid template and locate the definition of the remove_item function. The original function is defined like so:

function remove_item(id) {
   document.getElementById('updates_'+id).value = 0;
   document.getElementById('cartform').submit();
}

The function receives as parameter some id, and uses it to set the LineItem Qty to zero. What id is this? The following HTML snippet taken from the cart template should provide a strong hint:

id="updates_{{ item.variant.id }}"

So, the id is the variant ID. Let's use it to update our cookie.

function remove_item(id) {
  document.getElementById('updates_'+id).value = 0;
  // Adding our own code here -- BEGIN
  var myJSON = jQuery.cookie('shopify_cart_attributes');
  var items = jQuery.evalJSON(myJSON);
  for (var i=0; i<items.length; i++) {        
    if (items[i].variant_id == id) {
      items.splice(i, 1);
      i--;
    }
  }
  var configuration = {expires: 14, path: '/', domain: '{{ shop.domain }}'};
  jQuery.cookie('shopify_cart_attributes', 
    jQuery.toJSON(items), 
    configuration);
  // -- END
  document.getElementById('cartform').submit();
}

In JavaScript, using splice() is the way to go to remove an item from an array when that item may not be located at the beginning or end of the array. When we do remove an item, we have to decrement our iterator, otherwise we will skip an item.

for (var i=0; i<items.length; i++) {        
  if (items[i].variant_id == id) {
    items.splice(i, 1);
    i--; // decrementing our iterator.
  }
}

Adding a note to your cart and modifying email templates

Adding a special instruction field to your cart page

Users who browse without JavaScript have been instructed to specify their chosen options in a special instruction text box on the cart page. Now, we will add this special instruction box. Open your cart.liquid template, and add the following XHTML/Liquid markup to it, between the table element and the div with id basket-right:

<div id ="basket-left">
  <div class="form-item">
    <p>
      <label for="note">If you have any special instructions relating to your order, 
      please enter them in this box:</label>
    </p>
    <p>
      <textarea name="note" id="note" rows="3" cols="70" value="{{ cart.note }}"></textarea>
    </p>
  </div>
</div>

Your cart page showing three cart items

From the Shopify Wiki: You have the option of adding a note field to your [cart form]. Usage of this feature is very flexible. The general idea is that you simply define an input field named “note” in the form that submits to ”/cart” in cart.liquid. When the order is submitted you will be shown the note at the top of the order details page in your admin area.

You may want to style your new added div element with CSS like so:

#basket-left {
  float: left;
}

Modifying the emails that are sent to you about the order

You can add the cart.attributes information to your New Order notifications, by modifying your New Order Notification template. In your Admin area, go to Preferences | Email and Notifications, and click on the New Order Notification link.

Edit the template by simply adding this code outside of the for loop:

{{ attributes }}
{{ note }}

You can edit other email templates in the same way.

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.