Support add-on products

Add-on products are optional extras, like monogramming, gift wrap, or warranties, which shoppers can purchase to customize or enhance purchases.

An add-on is a product that you can link to a main product so shoppers see it on the main product’s details page and can optionally purchase it along with the main product. See Create add-on products to learn how to create an add-on product and link it to a main product.

You must make changes to several storefront layouts to allow your store to support add-on products. The modifications described in this section involve adding new widgets to page layouts and also making sure the latest versions are used for some widgets that are included in the page layouts out of the box. To determine if you are using the latest version, or to replace a widget with the latest version, see Customize your store layouts.

The following widgets incorporate add-on products functionality into your storefront:

  • The Order Confirmation and Order Details widgets have been updated to support add-on products. Make sure you are using the latest version of these widgets, which allow a shopper to see any add-on products that are part of the order.
  • The Product Details widget must be updated to display add-on products that shoppers can select, customize (if appropriate), and add to the cart along with the main product. See Product Details widget for add-ons for more information, including a sample version of the Product Details widget that lets shoppers select different types of gift wrapping and add a custom gift message.
  • The Shopping Cart and Cart Summary widgets have been updated to support add-on products. Make sure you are using the latest versions of these widgets. The new version of the Shopping Cart Summary widget allows a shopper to see, but not remove or edit, any add-on products that are part of the order. See Cart Summary widget for add-ons for a sample version of the Shopping Cart Summary widget that lets shoppers remove or edit add-on products.

Product Details widget for add-ons

There is no one-size-fits-all solution for displaying add-on products in the Product Details layout, so by default, the Product Details layout does not include components for add-on products. To allow shoppers to see and purchase add-on products, you must customize the layout’s Product Details widget. This section describes an example based on the Product Details widget that is included in Commerce. The sample updates the widget so it displays the details about all add-on products linked to the main product when the shopper views the main product’s details page. If an add-on product offers multiple SKUs, the shopper can select a SKU. If an add-on product allows shopper input, such as a gift message, the shopper can specify that value.

This sample assumes that you have already created and linked add-on products as described in Create add-on products. The add-on products in this sample include two product types, whose IDs are Warranty and GiftWrap. The GiftWrap product type includes a short text Shopper Input property that allows the shopper to add a gift message. Note that the code in this section is for illustrative purposes only; it is not intended to be production-ready, and may not adequately handle all possible use cases or implement the exact behavior you want. In addition, you may need to customize other widgets that handle add-on items.

Access add-on properties via the productTypesViewModel

The productTypesViewModel is populated with the ProductTypes data available from the data initializer. This view model is cacheable, and maintains a cache of ProductTypes data. The productTypesViewModel supports the following methods:

  • getInstance (data) gets the instance of the productTypesViewModel object. data is an optional object that contains the array of productTypes information.
  • setContextData (data) populates the productTypes list from the data fetched from Repositorydata. data is an object that contains the array of productTypes information.
  • retrieveShopperInputsData (productTypes, success, error) gets the ShopperInput for the requested productTypes.

Note: The dynamicProperty view model is used to store the shopperInput data.

Create an element to display an add-on product

The out-of-the-box version of the Product Details widget is separated into elements. (See Fragment a Widget into Elements for more information.) To create an element to display the add-on products, this sample’s template.txt file provides the HTML rendering code for the element.

<!-- ko if: initialized() -->
<div class="col-md-12">
  <!--  ko if: $data.addOnPopulated -->
  <div data-bind="foreach: addOnProducts">
    <br>
    <div style="border: .5px solid #a1a1a1;padding: 10px 40px;border-radius:
 7px;display: inline-block;background-color: #EEEEEE;margin-bottom: 10px;"
 class="col-md-12">
      <div class="col-md-12" style="left: -10px;">
        <input type="checkbox" data-bind="checked: isSelected, disable:
 (stockStatus != 'IN_STOCK' )" />
          &nbsp;<span data-bind="text: $data.displayName"></span>
      </div>
      <div class="col-md-12" data-bind="if: isSelected">
        <div class="col-md-12">
        <!-- ko with: $data.shopperInput -->
          <!-- ko foreach: $data -->
            <label data-bind="text: $data.label"></label>
            <!-- ko if: ($data.uiEditorType() == "shortText") -->
              <input  class="form-control"
type="text" data-bind="validatableValue: $data.value"><br>
            <!-- /ko -->
            <!-- ko if: ($data.uiEditorType() == "longText") -->
              <textarea class="form-control" data-bind="validatableValue:
 $data.value"></textarea><br>
            <!-- /ko -->
            <!-- ko if: ($data.uiEditorType() == "number") -->
              <input  class="form-control"
type="number" data-bind="validatableValue: $data.value"><br>
            <!-- /ko -->
            <!-- ko if: ($data.uiEditorType() == "date") -->
              <input  class="form-control" type="date"
data-bind="validatableValue: $data.value"><br>
            <!-- /ko -->
            <!-- ko if: ($data.uiEditorType() == "checkbox") -->
              <input  class="form-control" type="checkbox" data-bind="checked:
 $data.value, validatableValue: $data.value"><br>
            <!-- /ko -->
            <!-- ko if: ($data.type() == "enumerated") -->
                 <select class="form-control" type="text"  data-bind="options:
 $data.values, optionsCaption: $parents[2].listShopperInputPlaceHolderText,
 validatableValue: $data.value" ></select><br>
            <!-- /ko -->
            <!-- Validation message place holder -->
            <div>
              <p class="text-danger" id="CC-shopperInput-error"
              data-bind="validationMessage: $data.value" role="alert"></p>

            </div>
          <!-- /ko -->
        <!-- /ko -->
        </div>
        <!-- ko if: ($data.addOnOptions && $data.addOnOptions.length > 0) -->

          <!-- ko if: ($data.addOnOptions[0].product.type == 'GiftWrap' ||
 $data.addOnOptions[0].product.type == 'Normal') -->
            <span data-bind="widgetLocaleText: 'optionsText' "></span><br>
            <div class="col-md-12" style="display: inline-flex;">
              <!-- ko foreach: $data.addOnOptions -->
                <div class="col-md-4">
                  <img class="imageSize" data-bind="productVariantImageSource:
 {src: $data.product, imageType: 'thumb', alt:$data.product.displayName,
 errorSrc:'/img/no-image.jpg', errorAlt:'No Image Found'}, click: $parents[2].addOnIconChanged.bind($parents[2], $parent)" /><br>
              <div style="display:block;word-break:break-all;width:100%;">
                <span data-bind="text: $data.sku.repositoryId"></span>
                <span>&nbsp;-&nbsp;</span>
                <span data-bind="currency: {price: $data.product.listPrice, currencyObj: $parents[2].site().selectedPriceListGroup().currency, nullReplace: $parents[2].priceUnavailableText(), prependNull: false}"></span>
                  </div>
                </div>
              <!-- /ko -->
            </div>
          <!-- /ko -->
          <!-- ko if: ($data.addOnOptions[0].product.type == 'Warranty') -->
            <div class="col-md-12">
              <!-- ko foreach: $data.addOnOptions -->
                <input type="radio" data-bind="id:{name: $data.repositoryId}, checked: $parent.selectedAddonSku, value: $data.repositoryId, click: $parents[2].addOnRadioChanged.bind($parents[2], $parent) ">
                  <span id="cc-add-on-product-name" data-bind="text : $data.sku.repositoryId "></span>
                  <span id="cc-add-on-product-price" data-bind="currency: {price: $data.product.listPrice, currencyObj: $parents[2].site().selectedPriceListGroup().currency, nullReplace: $parents[2].priceUnavailableText(), prependNull: false}"></span>
                </input><br>
              <!-- /ko -->
            </div>
          <!-- /ko -->
        <!-- /ko -->
        <div class="col-md-12" class="text-danger" >
          <br>
          <span data-bind="text: $data.stockValidationMessage "></span>
        </div>
      </div>
    </div>
  </div>
  <!-- /ko -->
</div>
<!-- /ko -->

In this sample element.json meta-data file, the element is made available for use by the Sample Product Details widget:

{
 "inline" : false,
 "supportedWidgetType" : ["sampleProductDetails"],
 "translations" : [
 {
  "language" : "en_EN",
  "title" : "addons",
  "description" : "Displaying add-on products in the product details widget"
 }
 ]
}

In order to use the new element in a widget, you need to add some additional tags to the widget’s display.template and widget.template files that enable the element to be rendered as part of the output page and to be managed on the administration interface Design page. If the widget has already been broken into elements, you will, at a minimum, need to add an oc section tag for the new element:

<!-- oc section: product-addOn -->
    <div data-bind="element: 'sample-product-addOn'"></div>
<!-- /oc -->

Add an add-on product to the cart

The cart-item view model has been updated to include the following new fields:

  • isAddOnItem is a Boolean that is set to true for add-on products. This distinguishes between add-on items and Oracle CPQ child items.
  • shopperInput is a place holder field to capture the shopper input value, such as a gift message.
  • configurablePropertyId is the repository ID of the ConfigurableProperty that corresponds to the selected add-on product.
  • configurationOptionId is the repository ID of the ConfigurationOption that corresponds to the selected add-on product.

When a shopper selects an add-on product displayed on Product Details page and clicks the Add To Cart button, the addItem method of CartViewModel is triggered for the main product data, which is also the case for products without add-ons. The new field selectedAddOnProductsObj contains information that describes the selected add-on products, and is passed to addItem with the product.

addItem iterates over the selectedAddOnProductsObj array and creates a new CartItem object corresponding to each selectedAddon object. isAddOnItem is set as true and shopperInput is populated if the add-on product contains shopper input data. If no add-on products were selected by the shopper, then the childItems property of main product is undefined.

The following sample method iterates over the add-on products structure and trims any options that the shopper did not select before adding the main product and add-on products to the cart.

processAddonBeforeAddtoCart: function(addOnProducts) {
  var selectedAddonProducts = [];
  for (var i=0; i<addOnProducts.length; i++) {
    selectedAddonProducts.push(ko.toJS(addOnProducts[i]));
  }

  // Set the add-on products ShopperInputs
  var iAddonProdsSize = selectedAddonProducts.length - 1;
  var iSelectedSKUsSize = 0;
  for (var i=iAddonProdsSize; i>=0; i--) {
    if(!selectedAddonProducts[i].isSelected) {
      selectedAddonProducts.splice(i, 1);
      continue;
    }

    var shopperInput = {};
    if (selectedAddonProducts[i].shopperInput &&
selectedAddonProducts[i].shopperInput.length > 0) {
      for(j=0; j<selectedAddonProducts[i].shopperInput.length; j++) {
        // If a shopperInput is not entered then no need to send this further
        if(selectedAddonProducts[i].shopperInput[j].value ||
(selectedAddonProducts[i].shopperInput[j].required &&
 selectedAddonProducts[i].shopperInput[j].value === false)) {
          shopperInput[selectedAddonProducts[i].shopperInput[j].id] =
 selectedAddonProducts[i].shopperInput[j].value;
        }
      }
    }

    iSelectedSKUsSize = selectedAddonProducts[i].addOnOptions.length - 1;
    for (var j=iSelectedSKUsSize; j>=0; j--) {
      if(!selectedAddonProducts[i].addOnOptions[j].isSelected) {
        selectedAddonProducts[i].addOnOptions.splice(j, 1);
        continue;
      }
      selectedAddonProducts[i].addOnOptions[j].shopperInput = shopperInput;
      selectedAddonProducts[i].addOnOptions[j].quantity = 1;
    }

    // If none of the config options are selected, there is no need
   //  to pass the ConfigProperty
    if(selectedAddonProducts[i].addOnOptions.length == 0) {
      selectedAddonProducts.splice(i, 1);
    }
  }
  return selectedAddonProducts;
},

Create the sample Product Details widget.json file

The sample’s widget.json file defines meta-data for the widget and should look something like this:

{
  "name": "Sample Product Details",
  "javascript": "product-details",
  "availableToAllPages": true,
  "i18nresources": "sampleProductDetails",
  "imports": [
    "product",
    "imageRootUrl",
    "loaded",
    "productVariantOptions",
    "productTypes"
  ],
  "config" : {
  }
}

Cart Summary widget for add-ons

By default, add-on products that appear in the Cart Summary cannot be edited. This section describes an example based on the Cart Summary widget that is included in Commerce. The sample updates the widget so shoppers can edit or remove add-on products that are already in the cart. Note that the code in this section is for illustrative purposes only; it is not intended to be production-ready, and may not adequately handle all possible use cases or implement the exact behavior you want. In addition, you may need to customize other widgets that handle add-on items.

This sample assumes that you have already created and linked add-on products as described in Create add-on products.

When a shopper clicks the Edit button for an add-on product (childItem) associated with a main product (cartItem), the click handler opens a modal dialog and passes the selected add-on product ID, and the main product cartItem productData.

The JavaScript file for the widget defines a displayEditAddonModal() function that implements the logic for the dialog:

displayEditAddonModal : function(mainItemProduct,
selectedAddOn, element) {
     var widget = this;
     //Modal related functionality
     $('#CC-addonSelectionpane').on('show.bs.modal', function() {
       widget.selectedAddOnChildItem = selectedAddOn;
       if(widget.addonProductsMap[mainItemProduct.id]) {
         // Add-on data is already present.
         // No need to construct the data
         var tempAddonData = widget.addonProductsMap[mainItemProduct.id];
         for(var i=0; i<tempAddonData.length; i++) {
           if(tempAddonData[i].repositoryId ==
selectedAddOn.configurablePropertyId) {
             widget.editedAddonData(tempAddonData[i]);
             for(var j=0; j<widget.editedAddonData().addOnOptions.length; j++) {
               if(widget.editedAddonData().addOnOptions[j].repositoryId ==
selectedAddOn.configurationOptionId) {
              widget.editedAddonData().addOnOptions[j].isSelected(true);
                 break;
               }
             }
             if(widget.editedAddonData().shopperInput) {
               for(var j=0; j<widget.editedAddonData().shopperInput.length; j++) {
                 var shopperInputId =
widget.editedAddonData().shopperInput[j].id();
                 if(selectedAddOn.shopperInput[shopperInputId])
{widget.editedAddonData().shopperInput[j].value
(selectedAddOn.shopperInput[shopperInputId]);
                 }
               }
             }
             widget.addOnPopulated(true);
             break;
           }
         }
       } else {
      widget.getAddOnProductData(mainItemProduct.id, selectedAddOn,
mainItemProduct.addOnProducts);
       }
     });
     $('#CC-addonSelectionpane').modal('show');
     $('#CC-addonSelectionpane').on('hidden.bs.modal', function() {
       widget.addOnPopulated(false);
       widget.editedAddonData(null);
       widget.selectedAddOnChildItem = null;
     });
   },

  <script type='text/html' id='expand-item'>
  <li style="display : inline;">
    <!-- Expanding the childItems -->
    <!-- ko if: !$data.childItems -->
      <!-- ko if: !$data.addOnItem -->
        <div><a data-bind="ccLink: productData, attr:
{ id: 'CC-shoppingCart-configDetails-' + $data.repositoryId}">
<span data-bind="text: displayName"></span></a>
        <!-- ko foreach: $data.selectedOptions -->
          <!-- ko if: $data.optionValue -->
            (<span data-bind="widgetLocaleText :
{value:'option', attr:'innerText', params:
{optionName: $data.optionName,
            optionValue: $data.optionValue}},
            attr: { id: 'CC-shoppingCart-childProductOptions-'+
$parents[0].productId + $parents[0].catRefId  +
($parents[0].commerceItemId ? $parents[0].commerceItemId: '') +
$parents[0].removeSpaces($data.optionValue)}">
            </span>)
          <!-- /ko -->
        <!-- /ko -->
        <span data-bind="currency: { price: $data.externalPrice(),
currencyObj: $widgetViewModel.site().selectedPriceListGroup().currency}">
</span> -x<span data-bind="text: quantity"></span>
        <!-- ko foreach: externalData -->
          <div>
            <small>
              <!-- ko with: values -->
                <span data-bind="text: $data.label"></span>:
                <span data-bind="text: $data.displayValue"></span>
              <!-- /ko -->
              <!-- ko if: actionCode -->
                (<span data-bind="text: actionCode"></span>)
              <!-- /ko -->
            </small>
          </div>
        <!-- /ko -->
        </div>
      <!-- /ko -->
      <!-- ko if: $data.addOnItem -->
       <!-- ko if: $data.productData -->
        <br>
        <div data-bind="attr: {id: 'CC-shoppingCart-productAddonItems-' +
$parent.productId + $parent.catRefId + $parent.commerceItemId + $index()}">
          <strong>
            <span data-bind="text: $data.productData().displayName"></span>
            <span>&nbsp; - &nbsp;</span>

ko if: ($data.detailedItemPriceInfo) -->
              <span data-
bind="currency:{price:$data.detailedItemPriceInfo()[0]
.detailedUnitPrice,
currencyObj:$parents[3].site().selectedPriceListGroup().currency}">
</span>
               <!-- /ko -->
            <a href="#" data-bind=" click:
$parents[3].handleRemoveAddonFromCart.bind($parents[3], $data) ">
              <img data-bind="widgetLocaleText :
{value:'handleRemoveAddonFromCart', attr:'alt'},
              attr:{id:'CC-shoppingCart-removeAddonItem-' + productId
+ catRefId + (commerceItemId ? commerceItemId: '') }"
src="/img/remove.png" alt="Remove">
            </a>
          </strong>
          <br>
          <!-- ko if: $data.shopperInput -->
            <!-- ko foreach: Object.keys($data.shopperInput) -->
              <span data-bind="text: $data"></span>
              <span>: &nbsp;</span>
              <span data-bind="text:
$parent.shopperInput[$data]"></span><br>
            <!-- /ko -->
          <!-- /ko -->
          <span data-bind="text: $data.productData().displayName"></span>
          <span>: &nbsp;</span>
          <span data-bind="text: $data.catRefId"></span>
          <a href="#" data-bind="
click:$parents[3].displayEditAddonModal.bind($parents[3], $parent,
$data)" tabindex="0" data-toggle="modal">
            <u><span data-bind="widgetLocaleText:
'editAddonsText'">Edit</span></u>
          </a>
          <br>
        </div>
      <!-- /ko -->
       <!-- /ko -->
    <!-- /ko -->
    <!-- ko if: $data.childItems -->

      <div class = "alignChild"><a data-bind="click:
$widgetViewModel.setExpandedFlag.bind($data, $element),
 attr: { href: '#CC-shoppingCart-configDetails-' +
$data.repositoryId}" data-toggle="collapse"
class="configDetailsLink collapsed"
role="configuration"></a> <a data-bind="ccLink: productData">
<span data-bind="text: displayName"></span></a>
        <!-- ko foreach: $data.selectedOptions -->
                <!-- ko if: $data.optionValue -->
                  (<span data-bind="widgetLocaleText :
{value:'option', attr:'innerText', params: {optionName:
$data.optionName,
                  optionValue: $data.optionValue}},
                  attr: { id: 'CC-shoppingCart-productOptions-'+
$parents[0].repositoryId +
$parents[0].removeSpaces($data.optionValue)}">
                  </span>)
                <!-- /ko -->
        <!-- /ko -->
        <!-- ko ifnot: ($data.expanded) -->
         <span data-bind="if: $data.expanded,currency:
{ price: $data.itemTotal(), currencyObj:
$widgetViewModel.site().selectedPriceListGroup().currency}">
</span> -x<span data-bind="text: quantity"></span>
        <!-- /ko -->
        <!-- ko if: ($data.expanded) -->
         <span data-bind="currency:
{ price: $data.externalPrice(), currencyObj:
$widgetViewModel.site().selectedPriceListGroup().currency}">
</span> -x<span data-bind="text: quantity"></span>
         <!-- /ko -->
        <!-- ko foreach: externalData -->
          <div>
            <small>
              <!-- ko with: values -->
                <span data-bind="text: $data.label"></span>:
                <span data-bind="text: $data.displayValue"></span>
              <!-- /ko -->
              <!-- ko if: actionCode -->
                (<span data-bind="text: actionCode"></span>)
              <!-- /ko -->
            </small>
          </div>
        <!-- /ko -->
        <ul data-bind="template: {name: 'expand-item',
foreach: $data.childItems}, attr:
{ id: 'CC-shoppingCart-configDetails-' + $data.repositoryId}"
class="collapse">
        </ul>
      </div>
    <!-- /ko -->
  </li>
  </script>
  <!-- /ko -->
<!-- /ko -->

The JavaScript file defines a cancelEditAddon() function that implements logic for closing the dialog without making changes to the selected add-on product:

cancelEditAddon : function() {
                // Modal related functionality
                $('#CC-addonSelectionpane').modal('hide');

The JavaScript file defines a continueEditAddon() function that implements logic for closing the dialog when the shopper clicks the Save button to save changes to the selected add-on product:

continueEditAddon : function() {
        var widget = this;
        // Modal related functionality
        $('#CC-addonSelectionpane').modal('hide');

        var configOptions = widget.editedAddonData().addOnOptions;
        for(var i=0; i<configOptions.length; i++) {
          if(configOptions[i].isSelected()) {
            widget.selectedAddOnChildItem.catRefId =
configOptions[i].sku.repositoryId;
            widget.selectedAddOnChildItem.configurationOptionId =
configOptions[i].repositoryId;
            if(widget.editedAddonData().shopperInput &&
widget.editedAddonData().shopperInput.length > 0) {
              var shopperInput = {};
              for(var j=0; j<widget.editedAddonData().shopperInput.length; j++) {
         // If a shopperInput is not entered then no need to send this further
                if(widget.editedAddonData().shopperInput[j].value() ||
(widget.editedAddonData().shopperInput[j].required() &&
widget.editedAddonData().shopperInput[j].value() === false)) {
                  shopperInput[widget.editedAddonData().shopperInput[j].id()] =
widget.editedAddonData().shopperInput[j].value();
                }
              }
              widget.selectedAddOnChildItem.shopperInput = shopperInput;
            }
          }
        }
        console.log(widget.selectedAddOnChildItem);
        // Use cart VM method to update the cart Item data
        widget.cart().editChildItemFromCart(widget.selectedAddOnChildItem);
      },

The JavaScript file defines a validateEditAddon() function that implements logic for validating the shopper’s changes to the add-on product:

validateEditAddon : function() {
        var widget = this;
        if(!widget.editedAddonData()) {
          // If the editedAddonData is not yet created,
          // then there is nothing to validate.
          return;
        }

        var addonProduct = widget.editedAddonData();
        // 1. Check if at least one Config Option is selected
        var isConfigOptionSelected = false;
        for(var i=0; i<addonProduct.addOnOptions.length; i++) {
          if(addonProduct.addOnOptions[i].isSelected()) {
            isConfigOptionSelected = true;
            break;
          }
        }
        if(!isConfigOptionSelected) {
          return false;
        }
        // 2. Validate Shopper Input
        if(addonProduct.shopperInput) {
          for(var i=0; i<addonProduct.shopperInput.length; i++) {
            if(!addonProduct.shopperInput[i].validateNow()) {
              return false;
            }
          }
        }
        return true;

The JavaScript file defines a handleRemoveAddonFromCart() function that implements logic for removing the selected add-on product from the cart:

handleRemoveAddonFromCart: function(childCartItem) {
        var widget = this;
        console.log("remove ..");
        widget.cart().removeChildItemFromCart(childCartItem, true);
      },

The widget’s display.template file contains the following code for rendering the dialog:

<!-- MODAL dialog for editing or removing an add-on product -->
  <div class="modal fade col-md-12" id="CC-addonSelectionpane"
tabindex="-1" role="dialog">
    <div class="modal-dialog cc-config-modal-dialog">
      <div class="modal-content">
        <div class="modal-header CC-header-modal-heading">
          <!--  ko if: $parent.addOnPopulated -->
            <h3 data-bind="text:$parent.editedAddonData()
.displayName "></h3>
          <!-- /ko -->
        </div>
        <div class="modal-body cc-modal-body">
          <!--  ko if: $parent.addOnPopulated -->
            <div class="col-md-12">
              <!-- ko with: $parent.editedAddonData().shopperInput -->
                <!-- ko foreach: $data -->
                  <label data-bind="text: $data.label"></label>
                  <!-- ko if: ($data.uiEditorType() == "shortText") -->
                    <input  class="form-control"
type="text" data-bind="validatableValue: $data.value"><br>
                  <!-- /ko -->
                  <!-- ko if: ($data.uiEditorType() == "longText") -->
                    <textarea class="form-control"
data-bind="validatableValue: $data.value"></textarea><br>
                  <!-- /ko -->
                  <!-- ko if: ($data.uiEditorType() == "number") -->
                    <input  class="form-control" type="number"
data-bind="validatableValue: $data.value"><br>
                  <!-- /ko -->
                  <!-- ko if: ($data.uiEditorType() == "date") -->
                    <input  class="form-control" type="date"
data-bind="validatableValue: $data.value"><br>
                  <!-- /ko -->
                  <!-- ko if: ($data.uiEditorType() == "checkbox") -->
                    <input  class="form-control" type="checkbox"
data-bind="checked: $data.value, validatableValue: $data.value"><br>
                  <!-- /ko -->
                  <!-- ko if: ($data.type() == "enumerated") -->
                    <select class="form-control" type="text"
data-bind="options: $data.values,
optionsCaption: $parents[2].listShopperInputPlaceHolderText,
validatableValue: $data.value" ></select><br>
                  <!-- /ko -->
                  <!-- Validation message place holder -->
                  <div>
                    <p class="text-danger" id="CC-shopperInput-error"
                    data-bind="validationMessage:
$data.value" role="alert"></p>
                  </div>
                <!-- /ko -->
              <!-- /ko -->
            </div>

            <br>
            <!-- ko if: ($parent.editedAddonData().addOnOptions.
length > 0) -->
              <!-- ko if:
($parent.editedAddonData().addOnOptions[0].product.type ==
'GiftWrap' || $parent.editedAddonData().addOnOptions[0].product.type ==
 'Normal') -->
             <div class="col-md-12" style="display: inline-flex;">
               <!-- ko foreach: $parent.editedAddonData().addOnOptions -->
                 <div class="col-md-3">
                        <img style="max-height: 75px;
max-width: 75px;min-height: 75px;min-width: 75px;" data-
bind="productVariantImageSource: {src: $data.product,
imageType: 'thumb', alt:$data.product.displayName,
errorSrc:'/img/no-image.jpg', errorAlt:'No Image Found'},
 click: $parents[1].addOnIconChanged.bind($parents[1],
$parents[1].editedAddonData()) "><br>
           <div style="display:block;word-break:break-all;width:100%;">
                 <span data-bind="text: $data.sku.repositoryId"></span>
                       <span>&nbsp;-&nbsp;</span>
                       <span data-bind="currency: {price:
$data.product.listPrice, currencyObj:
$parents[1].site().selectedPriceListGroup().currency, nullReplace:
$parents[1].priceUnavailableText(), prependNull: false}"></span>&nbsp;
                           </div>
                    </div>
                  <!-- /ko -->
                </div>
              <!-- /ko -->
              <!-- ko if:
($parent.editedAddonData().addOnOptions[0].product.type == 'Warranty') -->
                <div class="col-md-12">
                  <!-- ko foreach:
$parent.editedAddonData().addOnOptions -->
                    <input type="radio" data-bind="attr:{id:
 $data.repositoryId, name:$parents[1].editedAddonData().repositoryId},
 checked: $parent.selectedAddonSku, value: $data.repositoryId, click:
 $parents[1].addOnRadioChanged.bind($parents[1],
 $parents[1].editedAddonData()) ">
                      <span id="cc-add-on-product-name"
data-bind="text: $data.sku.repositoryId "></span>
                      <span id="cc-add-on-product-price"
data-bind="currency: {price: $data.product.listPrice,
currencyObj: $parents[1].site().selectedPriceListGroup().currency,
 nullReplace: $parents[1].priceUnavailableText(), prependNull:
 false}"></span>
                    </input><br>
                  <!-- /ko -->
                </div>
              <!-- /ko -->
                 <div class="col-md-12" class="text-danger" >
                <br>
                <span data-bind="text:
$parent.editedAddonData().stockValidationMessage "></span>
              </div>
            <!-- /ko -->
          <!-- /ko -->
        </div>
        <div class="modal-footer CC-header-modal-footer">
          <button data-bind="click: $parent.cancelEditAddon"
type="button" class="cc-button-secondary">Cancel</button>
          <button data-bind="enable:
$parent.validateEditAddon.bind($parent)(), click:
$parent.continueEditAddon.bind($parent, $parent.editedAddonData())"
type="button" class="cc-button-primary">Save</button>
        </div>
      </div>
      <!-- /.modal-content -->
    </div>
    <!-- /.modal-dialog -->
  </div>
  <!-- /.modal -->