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.
  }
}