type:
snippet
category:
Misc cart and checkout
name:
Product Bundles/Addons
versions:
0.7.0, 0.7.1, 0.7.2, 1.0
reference:
http://forum.foxycart.com/comments.php?DiscussionID=1572&page=1#Comment_25071
tags:
Advance, Snippets, cart and checkout
date:
2011-05-27

Product Bundles/Addons

Packaged, bundled or addon products can be a great way to create special product offers or include required fees or costs with a purchase. This is especially useful when a customer is adding a subscription to a service that requires a once off setup fee to also be purchased at the same time. Or if a free product is being given to customers, but only if certain products are purchased at the same time. By utilising the following script, these scenarios are completely possible, allowing you to bundle any number of parent products to any number of dependent products with or without a matching quantity.

Step 1: Set up your products

For any products that you plan to use with this script, ensure that they all have unique product codes associated with them. For bundled products, this script leans towards them being added at the same time, through the same add-to-cart link or form. For adding multiple products in one click here for more information.

Step 2: Add Javascript

Paste the following code right before the closing </head> tag of your cart template.

<style type="text/css" media="screen">
  input.fc_readonly {
    color:#888;
    border:1px solid #CCC;
    background:#F1F1F1;
  }
  tr.fc_dependent td {
    background:#F1f1f1 !important;
  }
  span.fc_dependent_text {
    color:#AAA;
    font-style:italic;
  }
</style>
<script type="text/javascript">
  jQuery(document).ready(function($){
    // Check that we're not editing a sub, this string needs to *exactly match* what is shown in the alert when editing a sub.
    if (fc_json["messages"]["warnings"] != "You are currently modifying a subscription.") {
 
      // {"parent":["parentCode1","parentCode2"], "dependent":["dependantCode1","dependantCode2"], "quantity-match":true}
      var productBundles = [
        {"parent":["seat"], "dependent":["base"], "quantity-match":true},
        {"parent":["tee","tee2"], "dependent":["freetee","freeteefee"], "quantity-match":true}
      ];
 
      for (var c = 0; c < productBundles.length; c++) {
        var parentFound = false;
        var parentName = [];
        var parentQuantity = 0;
        for (var pc = 0; pc < productBundles[c]["parent"].length; pc++) {
          for (var p = 0; p < fc_json.products.length; p++) {
            // setup parent elements
            if (productBundles[c]["parent"][pc] == fc_json.products[p].code) {
              parentFound = true;
              parentName.push(fc_json.products[p].name);
              var parentChildren = jQuery("input[value='"+fc_json.products[p].id+"']").parent("td").next("td.fc_cart_item_quantity");
              parentQuantity += parseInt(jQuery(parentChildren).children("input").val());
              jQuery(parentChildren).children("input").addClass("parent-"+c)
 
              if(productBundles[c]["quantity-match"]) {
                jQuery(parentChildren).children("input").unbind().change(function() {
                  var id = jQuery(this).attr('class').match(/parent-(\d+)/)[1]
                  jQuery("input.child-"+id).val(totalQuantity(".parent-"+id));
                  fc_TestCheckout();
                }).keyup(function(event){
                  var id = jQuery(this).attr('class').match(/parent-(\d+)/)[1]
                  jQuery("input.child-"+id).val(totalQuantity(".parent-"+id));
                  fc_TestCheckout(event);
                });
              }
              jQuery(parentChildren).children("span").children("a").attr("onclick","").click(function() {
                var id = jQuery(this).parent("span").siblings("input.fc_cart_item_quantity").attr('class').match(/parent-(\d+)/)[1];
                jQuery(this).parent("span").siblings("input.fc_cart_item_quantity").val(0);
                if (jQuery("input.parent-"+id).length == 1 || totalQuantity(".parent-"+id) == 0) { // Only remove the children product if this is the only parent
                  jQuery("input.child-"+id).val(0).attr("readonly", true).addClass("fc_readonly");
                } else {
                  jQuery("input.child-"+id).val(totalQuantity(".parent-"+id));
                }
                fc_TestCheckout();
              });
              p = fc_json.products.length; // Finishes the loop
            }
          }
        }
 
        // Find dependent fields
        var dependentMissing = false;
        for (var d = 0; d < productBundles[c]["dependent"].length; d++) {
          var dependentFound = false;
          for (var p = 0; p < fc_json.products.length; p++) {
            if (productBundles[c]["dependent"][d] == fc_json.products[p].code) {
              var dependentFound = true;
              jQuery("input[value="+fc_json.products[p].id+"]").parent("td").next("td")
              var dependentChildren = jQuery("input[value="+fc_json.products[p].id+"]").parent("td").next("td.fc_cart_item_quantity");
              jQuery(dependentChildren).children("input").addClass("child-"+c);
              jQuery(dependentChildren).children("span").hide();
              jQuery(dependentChildren).parent("tr").addClass("fc_dependent");
              jQuery("input[value="+fc_json.products[p].id+"]").siblings(".fc_cart_item_options").before("<span class=\"fc_dependent_text\"> - added with '" + parentName.join("', '") + "'</span>");
              if (productBundles[c]["quantity-match"]) {
                jQuery(dependentChildren).children("input").attr("readonly", true).addClass("fc_readonly");
                if (jQuery(dependentChildren).children("input").val() != parentQuantity) {
                  jQuery(dependentChildren).children("input").val(parentQuantity);
                  showError("The product '"+fc_json.products[p].name+"' must have a matching quantity to '" + parentName.join("' and '") + "'. The quantity has been updated, please update the cart to save the new quantity.");
                  fc_PreventCheckout();
                }
              }
              p = fc_json.products.length; // Finishes the loop
            }
          }
          if (dependentFound === false) {
            dependentMissing = true;
          }
        }
        if ((parentFound === false && dependentMissing === false) || (parentFound === true && dependentMissing === true)) {
          if (parentFound === false && dependentMissing === false) {
            showError("Parent product missing. Please re-add the product with the code(s) '" + productBundles[c]["parent"].join("' or '") + "'.");
          } else if (parentFound === true && dependentMissing === true) {
            showError("A required addon product for '" + parentName.join("' and '") + "' is missing. Please re-add the product.");
          }
          jQuery("input.parent-"+c+", input.child-"+c).val(0).attr("readonly", true).addClass("fc_readonly");
          fc_PreventCheckout();
        }
      }
    }
  });
  function showError(text) {
    if ($('#fc_error_container ul li').length) {
      jQuery("#fc_error_container ul").append("<li>"+text+"</li>");
    } else {
      jQuery("table#fc_cart_table").before("<div id=\"fc_message_container\"><div id=\"fc_error_container\" class=\"fc_message fc_error\"><ul><li>"+text+"</li></ul></div></div>");
    }
  }
  function totalQuantity(ident) {
    var result = 0;
    jQuery(ident).each(function() {
      result += parseInt(jQuery(this).val());
    });
    return result;
  }
</script>

Step 3: Customise the javascript

Now that you have the script in place, you need to let it know what products it needs to look out for. Look for the productBundles in the script, and edit/add to the object. Adding another bundle is quite simple, simply follow this structure:

{"parent":["parentCode1","parentCode2"], "dependent":["dependantCode1","dependantCode2"], "quantity-match":true}
parent
Description: The codes for your parent products.
Type: An array of strings, comma seperated
Example: parent:[“code1”,”code2”,”code3”]
Notes: Can take any number of codes.
dependent
Description: The codes for your dependent products.
Type: An array of strings, comma seperated
Example: dependent:[“code1”,”code2”,”code3”]
Notes: Can take any number of codes.
quantity-match
Description: If true, the script requires that all dependant products in this set match the combined total of their matched parents and dependant product quantity inputs are set to read only.
Type: Boolean
Example: quantity-match:false
Notes: Only accepts true or false

Note: If you are defining multiple bundles, they must be comma separated (each one must end in a comma apart from the last)

How it works

The script works by completing the following set of actions on cart load:

  1. Loops through the productBundles object
  2. Loops through the parent products array for the current productBundles object
  3. Loops through fc_json.products looking for the parent product
    1. Overwrites the delete function and the change and keyup functions if quantity-match is set to true
    2. Sets a variable noting if the product was found
  4. Loops through the dependent products array for the current productBundles object
  5. Loops through fc_json.products looking for each dependent product
    1. Sets the quantity input to read only and adds a fc_readonly class if the bundle is set to quantity-match
    2. Hides the delete button
    3. Adds a fc_dependent class to the parent tr
    4. Adds a span to the dependent products name stating which parent product(s) it came from
    5. If quantity-match is set to true, compares its quantity to the parent(s) quantity
      1. If different, updates the quantity (to match the combined total of its parent(s), shows an error and prevents checkout
    6. Sets a variable noting if the variable was not found
  6. If a parent or dependent product was not found, shows an error message, makes the parent product read only with a class fc_readonly and prevents checkout

Site Tools