Create a widget to support saved carts
To enable and manage saved carts for a registered individual or account-based shopper, you write a custom widget to include on your storefront’s Cart pages.
For detailed information about creating widgets, see Create a Widget.
This topic includes the following sections:
-
Create the widget structure for the saved-carts sample widget
-
Create the JavaScript file for the saved-carts sample widget
Create the widget structure for the saved-carts sample widget
Widgets that include user interface elements must include display templates. The following shows an example of the files and directories in a saved-carts widget. Notice that it includes two display templates; these are described in detail in Create template files for the saved-carts sample widget.
MultiCartDemoWidget/
ext.json
widget/
multiCart_v1/
widget.json
js/
multi-cart.js
less/
widget.less
locales/
en/
ns.multicart.json
templates/
display.template
pagination.template
Because this widget includes user interface elements that allow the shopper to work with saved carts, you must not create it as a global widget. Set the global property in the widget.json file to false:
"global": falseThe JavaScript code you write extends the multiCartViewModel class. For more information about the widget structure and the contents of the ext.json and widget.json files, see Create the widget structure .
Create the JavaScript file for the saved-carts sample widget
The widget’s JavaScript file includes functions that let shoppers save, retrieve, merge, and delete carts:
listIncompleteOrdersgets all the incomplete orders (saved carts) associated with the logged-in shopper profile.createOrderWithTemporaryItemscreates an incomplete order for a shopper who has not logged in.createNewIncompleteCartcrates a new saved cart for the logged-in shopperloadParticularIncompleteOrderdisplays a saved cart.mergeWithParticularIncompleteOrdermerges a saved cart with the current cart.deleteParticularIncompleteOrdersdeletes a saved cart.
The following example shows sample JavaScript that implements the saved-cart functionality:
define(
//-------------------------------------------------------------------
// DEPENDENCIES
//-------------------------------------------------------------------
['knockout', 'pubsub', 'notifier', 'CCi18n', 'ccConstants',
'navigation', 'ccRestClient','viewModels/multiCartViewModel'],
//-------------------------------------------------------------------
// MODULE DEFINITION
//-------------------------------------------------------------------
function(ko, pubsub, notifier, CCi18n, CCConstants, navigation,
ccRestClient, MultiCartViewModel) {
"use strict";
return {
WIDGET_ID: "multiCart",
display: ko.observable(false),
currentCartName: ko.observable(""),
fetchSize: ko.observable(10),
cartNameSearch: ko.observable(""),
onLoad: function(widget) {
var self = this;
widget.listingViewModel = ko.observable();
widget.listingViewModel(new MultiCartViewModel());
widget.listingViewModel().itemsPerPage = widget.fetchSize();
widget.listingViewModel().blockSize = widget.fetchSize();
$.Topic(pubsub.topicNames.USER_AUTO_LOGIN_SUCCESSFUL)
.subscribe(function(){
widget.listIncompleteOrders();
});
$.Topic(pubsub.topicNames.USER_LOGIN_SUCCESSFUL).subscribe(function(){
widget.listIncompleteOrders();
if(widget.cart().items().length>0){
widget.cart().isCurrentCallInProgress = true;
widget.createOrderWithTemporaryItems();
}
});
$.Topic(pubsub.topicNames.CART_PRICE_SUCCESS).subscribe(function(){
if(widget.user().loggedIn()){
widget.listIncompleteOrders();
widget.currentCartName("");
}
});
$.Topic(pubsub.topicNames.CART_DELETE_SUCCESS).subscribe(function(){
if(widget.user().loggedIn()){
widget.listIncompleteOrders();
}
});
widget.listOfIncompleteOrders = ko.computed(function() {
var numElements, start, end, width;
var rows = [];
var orders;
var startPosition, endPosition;
// Get the orders in the current page
startPosition = (widget.listingViewModel().currentPage()
- 1) * widget.listingViewModel().itemsPerPage;
endPosition = startPosition +
parseInt(widget.listingViewModel().itemsPerPage,10);
orders = widget.listingViewModel().data.slice(startPosition,
endPosition);
if (!orders) {
return;
}
numElements = orders.length;
width = parseInt(widget.listingViewModel().itemsPerRow(), 10);
start = 0;
end = start + width;
while (end <= numElements) {
rows.push(orders.slice(start, end));
start = end;
end += width;
}
if (end > numElements && start < numElements) {
rows.push(orders.slice(start, numElements));
}
return rows;
}, widget);
},
beforeAppear: function (page) {
var widget = this;
if (widget.user().loggedIn() == false) {
widget.display(false);
} else {
widget.listIncompleteOrders();
widget.display(true);
}
},
/**
* @function
* @name multi-cart#listIncompleteOrders
*
* call to list incomplete orders for logged in profile.
*/
listIncompleteOrders : function() {
var self = this;
var inputDate ={};
//inputDate[CCConstants.SORTS] = "lastModifiedDate:desc";
self.listingViewModel().sortProperty = "lastModifiedDate:desc";
//set self.listingViewModel().cartNameSearch
//string to search based on cartname
if (self.user() && !self.user().loggedinAtCheckout()) {
self.listingViewModel().refinedFetch();
}
},
/**
* @function
* @name multi-cart#createOrderWithTemporaryItems
*
* method to create new incomplete cart with anonymous cart items
*/
createOrderWithTemporaryItems : function() {
var self = this;
self.cart().createNewCart(true);
self.cart().validateServerCart();
self.cart().getProductData();
self.cart().createCurrentProfileOrder();
},
/**
* @function
* @name multi-cart#createNewIncompleteCart
*/
createNewIncompleteCart : function() {
var self = this;
self.cart().createNewCart(true);
ccRestClient.setStoredValue(CCConstants.LOCAL_STORAGE_CREATE_NEW_CART,true);
self.cart().emptyCart();
self.user().orderId('');
self.user().persistedOrder(null);
self.user().setLocalData('orderId');
self.currentCartName("");
},
deleteParticularIncompleteOrders: function(pOrderId) {
var self = this;
self.cart().deleteParticularIncompleteOrders(pOrderId);
},
/**
* @function
* @name UserViewModel#loadParticularIncompleteOrder
*/
loadParticularIncompleteOrder : function(pOrderId) {
var self = this;
self.cart().loadCartWithParticularIncompleteOrder(pOrderId);
},
/**
* @function
* @name UserViewModel#mergeWithParticularIncompleteOrder
*/
mergeWithParticularIncompleteOrder : function(pOrderId) {
var self = this;
self.cart().mergeCartWithParticularIncompleteOrder(pOrderId);
},
saveIncompleteCart : function(pOrderId) {
var self = this;
self.cart().cartName(self.currentCartName());
self.cart().priceItemsAndPersist();
}
};
}
);
Create template files for the saved-carts sample widget
The widget’s display.template file contains code that renders a page where shoppers can see a list of saved carts, display a saved cart, merge a saved cart with the current cart, or delete a saved cart.
The widget’s display.template file contains the following code for rendering the page:
<!-- ko if: display-->
<!-- ko with: cart -->
<!-- ko if:($parent.user().loggedInUserName() && ($parent.user().loggedIn()
|| $parent.user().isUserSessionExpired()))-->
<div id="CC-multiCart">
<div class="row col-md-12">
<h3 class="modal-title text-center">Your Saved Carts</h3>
</div>
<div id="CC-multicartorder-table-md-lg-sm" class="row hidden-xs">
<section id="orders-info" class="col-md-12" >
<table class="table" >
<thead>
<tr>
<th class="col-md-2 " scope="col" data-bind="widgetLocaleText :
'orderNumber'"></th>
<th class="col-md-2 " scope="col" data-bind="widgetLocaleText:
'cartName'"></th>
<th class="col-md-2 " scope="col" data-bind="widgetLocaleText:
'orderTotal'"></th>
<th class="col-md-3" scope="col"><div class="sr-only"></div></th>
<th class="col-md-3" scope="col"><div class="sr-only"></div></th>
<th class="col-md-3 " scope="col" data-bind="widgetLocaleText:
'delete'"></th>
</tr>
</thead>
<!-- ko if: $parent.listOfIncompleteOrders().length > 0 -->
<tbody data-bind="foreach:$parent.listOfIncompleteOrders">
<tr>
<td class="col-md-2" data-bind="text : $data[0].orderId"
scope="row"></td>
<td class="col-md-2" data-bind="text : $data[0].cartName"
scope="row"></td>
<td class="col-md-2" data-bind="currency: {price: $data[0].total,
currencyObj: $data[0].priceListGroup.currency}" scope="row"></td>
<td class="col-md-3">
<button class="cc-button-primary pull-right" href="#"
data-dismiss="modal"
data-bind="click:$parents[1].loadParticularIncompleteOrder.bind($parents[1],
$data[0].orderId)" >
<span data-bind="widgetLocaleText: 'LoadThis'
,attr: {title: 'Clicking this will clear the cart and load this order'}">
</span>
</button>
</td>
<td class="col-md-3">
<button class="cc-button-primary pull-right" href="#"
data-dismiss="modal" data-bind="click:$parents[1].mergeWithParticularIncompleteOrder.bind($parents[1],
$data[0].orderId)" >
<span data-bind="widgetLocaleText: 'MergeInto',attr:
{title: 'Clicking this will merge the cart items into this order'}"></span>
</button>
</td>
<td class="col-md-3">
<button class="cc-button-primary pull-right" data-bind="click:$parents[1].deleteParticularIncompleteOrders.bind($parents[1],
$data[0].orderId)" >
<span data-bind="widgetLocaleText: 'delete' ,attr:
{title: 'Clicking this will delete this cart'}"></span>
</button>
</td>
</tr>
</tbody>
<!-- /ko -->
<!-- ko if: $parent.listOfIncompleteOrders().length == 0 -->
<tbody>
<tr>
<td colspan="5">
<span data-bind="widgetLocaleText:'noOrders'">
</span></td>
</tr>
</tbody>
<!-- /ko -->
</table>
</section>
</div>
<!-- ko with: $parent.listingViewModel -->
<div id="cc-paginated-controls-bottom"
class="row col-md-12 visible-xs visible-sm visible-md visible-lg">
<div data-bind="visible : (totalNumberOfPages() > 1)">
<div>
<div data-bind="template: { name:
$parents[1].templateAbsoluteUrl('/templates/paginationControls.template')
, templateUrl: ''}"
class="row pull-right"></div>
</div>
</div>
</div>
<!-- /ko -->
<div class="row col-md-12">
<!-- ko if: $data.items().length == 0 -->
<button type="button" class="btn btn-default"
data-bind="click:$parent.createNewIncompleteCart.bind($parent)">
Create New</button>
<!-- /ko -->
<!-- ko if: $data.items().length > 0 -->
<section id="cart-details-heading" >
<h3 class="modal-title text-center"
data-bind="widgetLocaleText:'currentCart'"></h3>
</section>
<section id="cart-info" class="col-md-12" >
Cart Name: <span data-bind="text: cartName"></span>
<table class="table" >
<thead>
<tr>
<th class="col-md-3 " scope="col" data-bind="widgetLocaleText:
'referenceId'"></th>
<th class="col-md-3 " scope="col" data-bind="widgetLocaleText:
'quantity'"></th>
<th class="col-md-3 " scope="col" data-bind="widgetLocaleText:
'total'"></th>
</tr>
</thead>
<tbody data-bind="foreach:$data.items" >
<tr>
<td class="col-md-3 text-left" data-bind="text :catRefId"
scope="row"></td>
<td class="col-md-3 text-left" data-bind="text :quantity()"
scope="row"></td>
<td class="col-md-3 text-left" data-bind="text :itemTotal()"
scope="row"></td>
</tr>
</tbody>
</table>
<input type="text" class="col-md-4 form-control"
name="currentCartName" id="currentCartName" data-bind="value:
$parent.currentCartName, widgetLocaleText : {value:'cartNameText',
attr:'placeholder'}">
<button type="button" class="btn btn-default"
data-bind="click:$parent.saveIncompleteCart.bind($parent)">Save Cart</button>
</section>
<section id="footer-buttons">
<button type="button" class="btn btn-default" data-bind="click:$parent.createNewIncompleteCart.bind($parent)">Create New</button>
</section>
<!-- /ko -->
</div>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
The widget’s display.template calls another template file, paginationControls.template. This template file contains .the following code for rendering multiple pages when the list of carts is long:
<div class="btn-group">
<a href="#" class="btn btn-default" data-bind="click:
getFirstPage, widgetLocaleText :
{value:'goToFirstPageText', attr:'aria-label'},
makeAccess: {readerText: 'Go to first page ', cssContent: 'on'},
css: { disabled: $data.currentPage() == 1 }, widgetLocaleText:
'goToFirstPagePaginationSymbol'" ><<</a>
<a href="#" class="btn btn-default" data-bind="click: decrementPage,
widgetLocaleText : {value:'goToPreviousPageText', attr:'aria-label'},
makeAccess: {readerText: 'Go to previous page ', cssContent: 'on'},
css: { disabled: $data.currentPage() == 1 }, widgetLocaleText:
'goToPreviousPagePaginationSymbol'" rel="prev"><</a>
<!-- ko foreach: pages -->
<a href="#" class="btn btn-default" data-bind="click:
$parent.changePage.bind($parent, $data), css: {active:
$data.pageNumber===$parent.clickedPage() }">
<!-- ko if: $data.selected === true -->
<span data-bind="widgetLocaleText : {value:'activePageText',
attr:'aria-label'}, makeAccess: {readerText: 'Active page is ',
cssContent: 'on'}"></span>
<!-- /ko -->
<!-- ko if: $data.selected === false -->
<span data-bind="widgetLocaleText : {value:'clickToViewText',
attr:'aria-label'}, makeAccess: {readerText: 'Click to view page ',
cssContent: 'on'}"></span>
<!-- /ko -->
<span data-bind="ccNumber: $data.pageNumber"></span>
</a>
<!-- /ko -->
<a href="#" class="btn btn-default" data-bind="click: incrementPage,
widgetLocaleText : {value:'goToNextPageText', attr:'aria-label'}, makeAccess:
{readerText: 'Go to next page ', cssContent: 'on'}, css: { disabled:
currentPage() == $data.totalNumberOfPages() }, widgetLocaleText:
'goToNextPagePaginationSymbol'" rel="next">></a>
<a href="#" class="btn btn-default" data-bind="click: $data.getLastPage,
widgetLocaleText : {value:'goToLastPageText', attr:'aria-label'}, makeAccess:
{readerText: 'Go to last page ', cssContent: 'on'}, css: { disabled:
currentPage() == $data.totalNumberOfPages() }, widgetLocaleText:
'goToLastPagePaginationSymbol'">>></a>
</div>