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 theproductTypesViewModel
object. data is an optional object that contains the array ofproductTypes
information.setContextData (data)
populates theproductTypes
list from the data fetched fromRepositorydata
. data is an object that contains the array ofproductTypes
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' )" />
<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> - </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 totrue
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 theConfigurableProperty
that corresponds to the selected add-on product.configurationOptionId
is the repository ID of theConfigurationOption
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> - </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>: </span>
<span data-bind="text:
$parent.shopperInput[$data]"></span><br>
<!-- /ko -->
<!-- /ko -->
<span data-bind="text: $data.productData().displayName"></span>
<span>: </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> - </span>
<span data-bind="currency: {price:
$data.product.listPrice, currencyObj:
$parents[1].site().selectedPriceListGroup().currency, nullReplace:
$parents[1].priceUnavailableText(), prependNull: false}"></span>
</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 -->