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:

shopping cart

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:

personalize item dialog

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:

splitting the line item

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 -->
shopping cart with split line item

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:

edit the custom property

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>