Modifying Item Variations in a StoreFront

This page will show how to customize item variations.  If you need item options instead, see this article: Modifying Item Options in a StoreFront

StoreFront Item Variations

This is no longer valid with the new visual builder storefronts. There is no need to edit or modify code directly and everything can now be configured within the element settings.


Converting an Item from using Options to Variations (introduction to variations)

Item Variations (instructions for creating/configuring variations)

Adding ?uc-debug to then end of an item page containing variations will generate debugging output in the browser console.

Example: https://demo.ultracartstore.com/shop/TSHIRT.html?uc-debug

HTML Components

The variations are found in two places. 

  1. The visible portions of the variations, such as the select boxes (or radio buttons), cost elements, and inventory spans or divs are found in the template_item.vm file.
  2. The javascript that powers the variations is (usually) found in the document_bottom.vm file.   This is a snippet file, so it is found in your /themes/<my theme name>/snippets/ directory and NOT in the templates directory tree.


Item Variations Costs

Item costs may be automatically updated by following the following naming scheme.  The javascript will search for matching elements and update their inner html with prices as they change with customer selections.

The javascript will also toggle the classes of cost and sale cost elements with 'has-sale' and 'no-sale' so you may stylize them as desired.  For examples, search your main.css file in your theme for those two classes.   You may also use event hooks (see below) to do just about anything you desire as cost changes.

#if($item.getMAPLocalized(false))
   <span class="price uc-variation-cost-${item.getMerchantItemID()}">$item.getMAPLocalized(false)</span>
#elseif($item.getSaleCost())
   <span class="#if($item.getSaleCost())has-sale#end price uc-variation-cost-${item.getMerchantItemID()}"><del>$item.getRegularCostLocalized()</del></span>&nbsp;&nbsp;&nbsp;
   <span class="#if(!$item.getSaleCost())no-sale#end sale price uc-variation-sale-cost-${item.getMerchantItemID()}">$item.getSaleCostLocalized()</span>
#else
   <span class="price uc-variation-cost-${item.getMerchantItemID()}">$item.getCostLocalized()</span>
#end

Item Variations Form Elements

#set($hasVariations = $item.getVariations().size() > 0)
#if($hasVariations)
  #foreach($variation in $item.getVariations())
	<label class="label-group"><span>${variation.getName()}</span>
	  <input type='hidden' name="VariationName${velocityCount}" value="${variation.getName()}"/>
	  <select name="VariationValue${velocityCount}" id="uc-variation-field-${velocityCount}-${item.getMerchantItemID()}" class='variationSelectBox'>
	  </select>
	</label>
  #end
#end


This code is part of the much larger <form> block within the template_item.vm file

The naming of the select boxes is crucial.  They must have the proper id or the variations will not work.  Do not deviate from that id naming structure for any reason.


Javascript Component

<script type="text/javascript" src="/catalog_5.1.js"></script>
 
<!-- there will be a lot of other html components in between the catalog_5.1.js reference and the actual item variation code ... just FYI -->
 
 
#if($item && $item.hasVariations() && "$cart" != "")
  <script type='text/javascript'>
  //this is dependent on jquery and catalog_5.1.js
  console.log('initializing catalog variations');
  #set($currencyCode = $cart.getCurrency().getSymbol())
  // this can be named anything or assigned anywhere.  Or not even assigned to a variable.
  // I just chose window.itemVariations so it would be easy to debug
  var itemVariationsOptions = {
    merchantItemId: "$item.getMerchantItemID()",
    variationMatrix: ${item.getVariationMatrix().getJavascriptItemVariationMatrix2(false, 80, 80)},
    multimedia: ${item.getMultimediaAsJson()},
    currencyCode: "$currencyCode",
    inventory: 'show-quantity',
    validation: function(missingFields){ 
      window.alert("Look jackball, select values for the following fields before continuing: " + missingFields.join(", ")); 
      return false; 
    },
    customOutOfStock: {'X-Large': 'out of season'},
    formatters: {
      'Color': function(formatData){
        var priceIsRanged = (formatData.cost != formatData.maxCost);
        var costStr = " " + formatData.costFormatted;
        if(priceIsRanged){
          costStr = " [" + formatData.costFormatted + " - " + formatData.maxCostFormatted + "]";
        }
         if(formatData.inventory > 0){
           return formatData.optionText + costStr;
         } else if(formatData.status == 'backorder') {
           return formatData.optionText + costStr + " (backorder" + (formatData.eta ? (" until " + formatData.eta + ")") : ")");
         } else if(formatData.status == 'made to order') {
           return formatData.optionText + costStr + " (made to order" + (formatData.leadTime ? (" in " + formatData.leadTime + " days)") : ")");
         } else if(formatData.status == 'preorder') {
           return formatData.optionText + costStr + " (preorder" + (formatData.eta ? (", ships " + formatData.eta + ")") : ")");
         } else {
           return formatData.optionText + costStr + " (" + formatData.status + ")";
        } //end-if inventory/status selections
      }, //end of 'Color' formatter
      'Size': function(formatData){
        var priceIsRanged = (formatData.cost != formatData.maxCost);
        var costStr = " " + formatData.costFormatted;
        if(priceIsRanged){
          costStr = " [" + formatData.costFormatted + " - " + formatData.maxCostFormatted + "]";
        }
         if(formatData.inventory > 0){
           return formatData.optionText + costStr;
         } else if(formatData.status == 'backorder') {
           return formatData.optionText + costStr + " (backorder" + (formatData.eta ? (" until " + formatData.eta + ")") : ")");
         } else if(formatData.status == 'made to order') {
           return formatData.optionText + costStr + " (made to order" + (formatData.leadTime ? (" in " + formatData.leadTime + " days)") : ")");
         } else if(formatData.status == 'preorder') {
           return formatData.optionText + costStr + " (preorder" + (formatData.eta ? (", ships " + formatData.eta + ")") : ")");
         } else {
           return formatData.optionText + costStr + " (" + formatData.status + ")";
        } //end-if inventory/status selections
      } //end of 'Size' formatter            
    } //end of formatters
  }; //end of item variation options hash.
  window.itemVariations = new ultracart.ItemVariations(jQuery, itemVariationsOptions);
    window.itemVariations.on('options-changed', function(event, new_options){ console.log('options-change event triggered', new_options); });
    window.itemVariations.on('cost-changed', function(event, cost_data){ console.log('cost-change event triggered', cost_data); });
    window.itemVariations.on('item-selected', function(event, item_data){ console.log('item-selected event triggered', item_data); });
    window.itemVariations.on('item-unselected', function(event){ console.log('item-unselected event triggered (no extra data provided)'); });                      
    window.itemVariations.on('multimedia-selected', function(event, multimediaOid){ 
      console.log('multimedia-selected event triggered, multimediaOid:', multimediaOid); 
      var multimediaImageIndex = parseInt(jQuery("li.product-image[data-multimedia-oid='" + multimediaOid + "']").attr('index'));
      if(!isNaN(multimediaImageIndex)){
         jQuery('#main-item-image-viewer-holder').slickGoTo(multimediaImageIndex);
      }                           
    });
    window.itemVariations.on('multimedia-unselected', function(event){             
      console.log('multimedia-unselected event triggered (no extra data provided)');            
      // revert back to the original image, which is always at index 0.
      jQuery('#main-item-image-viewer-holder').slickGoTo(0);
    });          
  </script>
#elseif($item && $item.hasOptions() && "$cart" != "")

   ## THIS WILL CONTAIN A LARGE CODE BLOCK FOR ITEM OPTIONS.  IT WAS REMOVED TO AVOID CONFUSION IN THIS DOCUMENTATION.

#end



 Options

inventory : 'show-quantity' | 'hide-out-of-stock'

show-quantity will display the quantity of inventory next to the option

hide-out-of-stock will remove any options that have an inventory less than or equal to 0.  this option will also exclude backorder and preorder items.

For find tune control, use 'show-quantity' and use custom formatters to render your options.


validation: function(missingFields)

this function is a form submit function, so it should return true/false to continue or prevent form submission.   missingFields is a comma separated string of fields that do not have selections.

if no validation is provided, a simple alert will display provided the form has a class of '.uc-variation-form-' + merchantItemId


customOutOfStock: { variationName1: 'custom message', variationName2: 'custom message2' }

This optional value allows for custom messages when a variation option is out of stock.  The default is 'out of stock'.  Some merchants sell seasonal items and would rather the message be 'out of season', etc.  Use this option to provide such messages.


formatters: { variationName1: function(formatData), variationName2: function(formatData) }

formatters take in formatData and return a string for the option value.  This string may contain html markup if radio buttons are used.  For select boxes (default), simple strings are the goal.

You may supply a generic formatter by specifying a variation name of "*". If so, this formater will be used if no specific formatter is found.

Example:

formatters: { 'Color': myColorFormatter(formatData), 'Size': mySizeFormatter(formatData), '*': myGenericFormatter(formatData) }

formatData properties:

propertyexamplescomments
optionText

'Small'

'Medium'

'Large'

this is the default text that will appear
inventory

0

1000


status

''

'out of stock'

if there is no status (i.e. the item has plenty of inventory), this value will be an empty string
cost

9.99

19.99

number
maxCost29.99 number, the max cost may be useful if your variation option has a range of prices. for example, if your option has a range of costs, you could use maxCost to show the range. Search the code example above for

priceIsRanged for an example

costFormatted$9.99 string
maxCostFormatted$19.99 string
originalCost19.99 number
originalCostFormatted$19.99 string
saleCost5.99 number
saleCostFormatted$5.99 string
hasSale

true

false


parentCost

9.99

19.99

number, useful for calculating price differentials, showing how much more or less this variations is compared to the parent variation.
parentCostFormatted$9.99 string
parentOriginalCost19.99 number
parentOriginalCostFormatted$19.99 string
parentSaleCost5.99 number
parentSaleCostFormatted$5.99 string
parentHasSale

true

false






Events

All events except the item-unselected and multimedia-unselected event will contain a data parameter (2nd parameter).

please notice that the variation event is plural (options-changed) while the options event is singular (option-changed)


Item Variation Events

options-changed

This event fires whenever an variation changes options (customer selects a different value).  

data:  

/**
* @typedef {Object} VariationEventOptionsChange
* this event will contain a hash of all options and their currently selected values. any unselected values will have
* a zero length string.
*/


The variations event above has a plural options, while the item options event has a singular option. Please note the difference.



cost-changed

This event fires whenever the minimum cost changes.

/**
* @typedef {Object} VariationEventCostChange
* @property {number} cost
* @property {string} costFormatted
* @property {number} originalCost
* @property {string} originalCostFormatted
* @property {number} saleCost
* @property {string} saleCostFormatted
* @property {boolean} hasSale true if these combination of items is on sale.
* @property {boolean} hasSelectedItem true if a single item is selected, else false
* @property {string} itemId will contain the item id of the item if it's selected, else null.
* @property {number} parentCost
* @property {string} parentCostFormatted
* @property {number} parentOriginalCost
* @property {string} parentOriginalCostFormatted
* @property {number} parentSaleCost
* @property {string} parentSaleCostFormatted
* @property {boolean} parentHasSale true if these combination of items is on sale.
* this event contains all relevant price data.
*/


item-selected

This event fires whenever enough variations have been selected to narrow down the possible choice of child items to one.  This is very useful for changing the main item image to the child item.

/**
* @typedef {Object} Variation This object would be better termed 'VariationItem', since each record represents a child variation of the top level item
* @property {string} itemId the merchant item id
* @property {number} cost The current cost of the variation item. Example 19.99
* @property {number} originalCost The original cost of this variation. Example: 29.99
* @property {number} [saleCost] The sale cost of this variation. Example: 29.99. Will be absent entirely if item is not on sale
* @property {boolean} hasSale true if this variation is on sale, false otherwise
* @property {number} inventory the inventory of this variation. if inventory is not tracked, this will be 1000
* @property {boolean} inventoryTracked true if inventory is tracked on this item
* @property {boolean} allowBackOrder true if this item allows backOrder
* @property {boolean} preorder true if this item is a preorder item
* @property {string} eta date string of the estimate time of arrival for backOrder or preorder
* @property {boolean} madeToOrder true if this item is made to order
* @property {integer} [leadTime] number of days required to make this item, will be missing if madeToOrder = false
* @property {Object} values an object of option values that match this variation. For example: Object { Size="Large", Color="Black"}
* @property {string} compositeKey a pipe delimited list of values in 'sort' order to allow for easy option value -> variation lookup.
*/

item-unselected

This event fires whenever the variations change and a single item is no longer selected.  For example, if the customers starts over with the first select box again, this will fire.  This is useful for reverting any image or description information back to the main parent item.


multimedia-selected

This event fires whenever a multimedia (image) is applicable to the currently selected variations.  This can occur based on a selection from one of the select boxes (or radio buttons), or when enough selections are made to narrow down to a single child item which has an assigned multimedia image.

/**
* @typedef {Object} Multimedia contains high level information about one of the multimedia objects associated with the parent item
* @property {string} type
* @property {boolean} isDefault
* @property {number} imageWidth
* @property {number} imageHeight
* @property {string} viewUrl
* @property {string} viewSsl
* @property {string} description
* @property {string} code
* @property {string} merchantItemId
* @property {boolean} excludeFromGallery
* @property {string} filename
* @property {number} multimediaOid
*/

multimedia-unselected

This event fires whenever the customer has changed their selections (or cleared them out) to the point where no images can be found to match the current set of variation choices.

This event does not have any extra data associated with it.