Implement a custom cart summary widget
After you add custom properties to the commerceItem
item type, you need to provide a way for a shopper to specify the values of these properties and to split individual line items into multiple line items for customization.
To enable a shopper to specify the values of these properties and to split individual line items into multiple line items for customization, you replace the Cart Summary widget on your shopping cart page with a custom widget that implements these options.
This section describes a custom widget that you could create to add these
capabilities to your storefront. It assumes that you have previously created the
monogram_initials
custom property shown in the Add custom properties to the commerceItem item type section. Note that
the code in this example 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 display order
data to handle split line items and custom properties.
Also, keep in mind that when you create a dynamic property for order line items, the property is added to all line items. You may not want to expose the property in all cases. For example, your store may offer monogramming only for certain items; for other items, you do not want a personalization option to appear. You may need some additional logic in your custom widget to conditionally expose or hide personalization options, depending on the item. For example, you could expose personalization options only for certain custom product types.
Display links for personalization
The custom widget’s
display.template
file conditionally displays one of two links for each
line
item:
<!-- ko ifnot: $parent.isPersonalized -->
<a data-bind="click: $parents[2].personalizeItem.bind($data, $parent,
$parents[2])" data-toggle="modal">Personalize</a>
<!-- /ko -->
<!-- ko if: $parent.isPersonalized -->
<a data-bind="click: $parents[2].editItem.bind($data, $parent, $parents[2])"
data-toggle="modal">Edit</a>
<!-- /ko -->
When the cart is initially displayed, none of the line items have been personalized, so the shopping cart page shows a Personalize link for each line item. For example:
Clicking a line item’s Personalize link opens a modal dialog for splitting the line item and personalizing the resulting items. For example, if the shopper clicks the Personalize link for the Organized Wallet line item, the following dialog is displayed:
If the checkbox is checked, the line item will not be split when the
shopper clicks Save, and the value the shopper supplies for the
monogram_initials
property will be applied to both wallets. If the
checkbox is unchecked, the line item will be split, and the dialog expands to display fields
for specifying the custom property values for each item individually:
After the shopper fills in the monogram values and clicks Save, the
Organized Wallet line item is split into two line items, and the
monogram_initials
property is set separately on each one. The widget’s
display.template
file displays the value of the property for each
item it is set
on:
<!-- ko if:($parents[1][$data.id()]) -->
<span data-bind = "text: $data.label"></span> : <span data-bind = "text:
$parents[1][$data.id()]"></span><br>
<!-- /ko -->
Notice that there are now two line items for the Organized Wallet, each with a quantity of 1, and each with a different value for the custom property. The Tumbler Glass line item still has a Personalize link, but the Organized Wallet line items now have Edit links instead. Clicking one of the Edit links opens a dialog for changing the monogram for the wallet associated with that link. For example:
Create the dialog for splitting and personalizing line items
The JavaScript file for the widget defines a personalizeItem()
function that implements the logic for the dialog:
personalizeItem: function(item, widget) {
//Personalizing the item
var totalQuantity = item.quantity();
if(widget.cart().lineAttributes().length > 0) {
for(var i=0; i< totalQuantity; i++) {
var propObj = {};
for(var j=0; j< widget.cart().lineAttributes().length;j++) {
//Injecting default values of properties from the metadata
propObj[widget.cart().lineAttributes()[j].id()] =
ko.observable(widget.cart().lineAttributes()[j].value());
}
//Pushing each key-value pair to the result object to show onto the modal
widget.itemProps.push(propObj);
}
}
//Modal related functionality
$('#cc-personalizationPane').on('show.bs.modal', function() {
widget.item(item);
});
$('#cc-personalizationPane').modal('show');
$('#cc-personalizationPane').on('hidden.bs.modal', function() {
widget.itemProps([]);
});
},
If the custom properties have default values, these values are used to populate the dialog fields. However, providing defaults for these values is not recommended, because they will be applied to all line items, including ones that cannot actually be personalized.
The widget’s display.template
file contains the following for rendering the dialog:
<!-- Personalization Modal -->
<div class="modal fade" id="cc-personalizationPane" tabindex="-1" role="dialog">
<div class="modal-dialog cc-modal-dialog">
<div class="modal-content">
<!-- ko if: $parent && $parent.item()!=null -->
<div class="modal-header CC-header-modal-heading">
<h4>Personalize your Item</h4>
</div>
<div class="modal-body cc-modal-body">
<h5>Item 1</h5>
<!-- ko with: lineAttributes -->
<!-- ko foreach: $data -->
<label class="control-label" data-bind="text: label"></label>
<!-- ko if: $parents[2].itemProps()[0] -->
<!-- ko if: uiEditorType() == "shortText" || uiEditorType() == "richText"
|| uiEditorType() == "number" || uiEditorType() == "date" -->
<input class="form-control" type="text" data-bind="attr: {name : id},
value: $parents[2].itemProps()[0][id()]"><br>
<!-- /ko -->
<!-- ko if: uiEditorType() == "checkbox" -->
<input class="form-control" type="checkbox" data-bind="attr: {name : id},
checked: $parents[2].itemProps()[0][id()]"><br>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<input type="checkbox" data-bind="checked: $parent.noRepeat">Use this
for all items</input>
<div data-bind="visible: !$parent.noRepeat()">
<!-- ko foreach: new Array($parent.item().quantity()-1) -->
<h5><p>Item <span data-bind="text: $index()+2" /></p></h5>
<!-- ko with: $parent.lineAttributes -->
<!-- ko foreach: $data -->
<label class="control-label" data-bind="text: label"></label>
<!-- ko if: $parents[3].itemProps()[$parentContext.$index()+1] -->
<!-- ko if: uiEditorType() == "shortText" || uiEditorType() == "richText"
|| uiEditorType() == "number" || uiEditorType() == "date" -->
<input class="form-control" type="text" data-bind="attr: {name : id},
value: $parents[3].itemProps()[$parentContext.$index()+1][id()]"/><br>
<!-- /ko -->
<!-- ko if: uiEditorType() == "checkbox" -->
<input class="form-control" type="checkbox" data-bind="attr: {name : id},
checked: $parents[3].itemProps()[$parentContext.$index()+1][id()]"/><br>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>
<div class="modal-footer CC-header-modal-footer">
<button data-bind="click: $parent.cancelPersonalization.bind($parent)"
type="button" class="cc-button-secondary">Cancel</button>
<button data-bind="click: $parent.savePersonalization.bind($parent)"
type="button" class="cc-button-primary">Save</button>
</div>
<!-- /ko -->
</div>
</div>
</div>
The JavaScript file for the widget also includes a savePersonalization()
function, which is executed when the shopper clicks Save:
savePersonalization: function() {
var widget= this;
//Saving personalized values
if(widget.noRepeat()) {
//If the flag is checked, populate the entire quantity with the same set
//of values.
widget.item().populateItemDynamicProperties(widget.itemProps()[0]);
widget.item().isPersonalized(true);
widget.cart().markDirty();
} else {
//Splitting all quantities to 1 each if the flag is unchecked.
//This can be customized further to split total quantity in any manner.
var quantityList = new Array(widget.item().quantity()+1).join(1).
split('').map(function(){return 1;})
//Calling split items function to create multiple lines with
//different custom properties provided.
widget.cart().splitItems(widget.item(), quantityList,
widget.itemProps());
}
//Modal related functionality
$('#cc-personalizationPane').modal('hide');
},
If the shopper chooses to split a line item, the widget splits it into line items whose quantity is 1. For example, if the line item has a quantity of 3, it is split into three line items with a quantity of 1. After an item is split, the shopper can increase the quantity of one of the resulting items and then split that item. If the shopper splits an item and then adds more of the same SKU to the shopping cart, the addition is treated as a separate line item and not combined with the split items.
Note that the splitItems()
function of the CartViewModel
supports splitting in other ways than the above code implements. For example, splitItems()
can split a line item with quantity 3 into two line items, one with a quantity of 1 and one with a quantity of 2. You can support this option in your own custom widget by creating controls that enable shoppers to specify different splitting options.
When the customer edits property values, the sample widget triggers one pricing call per edit. You can reduce the number of pricing calls by implementing a way for your custom widget to trigger pricing only after all personalization is complete.
Create the dialog for modifying personalized line items
The isPersonalized
boolean on the CartItem
is used
to indicate whether a line item has been personalized. By default it is set to
false
; when a shopper clicks a Personalize link on a line item to invoke
the widget’s personalizeItem()
function, the widget sets the
isPersonalized
property to true
. This causes the Edit
link to be displayed for the resulting line items. Clicking the Edit link invokes the
widget’s updatePersonalization()
function, which enables further changes to
the custom property values, but not further splitting of the line items:
updatePersonalization: function(){
var widget = this;
//Calling the method to update properties of the item specified
//by the user in the modal
widget.item().populateItemDynamicProperties(widget.itemProps()[0]);
$('#cc-editPane').modal('hide');
widget.cart().markDirty();
},
You could extend this function to support further splitting of line items as well.
The widget’s display.template
file contains the following for
rendering the dialog:
<!-- Edit Personalization Modal -->
<div class="modal fade" id="cc-editPane" tabindex="-1" role="dialog">
<div class="modal-dialog cc-modal-dialog">
<div class="modal-content">
<!-- ko if: $parent && $parent.item()!=null -->
<div class="modal-header CC-header-modal-heading">
<h4>Edit Personalization</h4>
</div>
<div class="modal-body cc-modal-body">
<h5>Item</h5>
<!-- ko with: lineAttributes -->
<!-- ko foreach: $data -->
<label class="control-label" data-bind="text: label"></label>
<!-- ko if: $parents[2].itemProps()[0] -->
<!-- ko if: uiEditorType() == "shortText" || uiEditorType() ==
"richText" || uiEditorType() == "number" || uiEditorType() == "date" -->
<input class="form-control" type="text" data-bind="attr: {name : id},
value: $parents[2].itemProps()[0][id()]"><br>
<!-- /ko -->
<!-- ko if: uiEditorType() == "checkbox" -->
<input class="form-control" type="checkbox" data-bind="attr: {name :
id}, checked: $parents[2].itemProps()[0][id()]"><br>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
<div class="modal-footer CC-header-modal-footer">
<button data-bind="click: $parent.cancelEdit.bind($parent)" type="button"
class="cc-button-secondary">Cancel</button>
<button data-bind="click: $parent.updatePersonalization.bind($parent)"
type="button" class="cc-button-primary">Save</button>
</div>
<!-- /ko -->
</div>
</div>
</div>