Implement split shipping UI controls
It is standard e-commerce practice that shipping selections are implemented as part of the checkout flow.
Commerce already implements single shipping this way, so for the purposes of continuity, it is recommended that split shipping is also implemented in checkout. A non-standard implementation (such as on the cart page), although possible, would require more custom coding and may have additional side effects that require mitigation.
Split shipping toggle
A split shipping toggle button allows users to activate or deactivate split shipping for the current order. The button toggles the state of the isSplitShipping
property. Also, it may be necessary to toggle the visibility of split shipping/single shipping UI elements.
The following sequence diagrams shows how you might choose to activate and deactivate the split shipping toggle on your storefront.
Activate
In this diagram, the following happens:
- The shopper clicks a Use Split Shipping button on the UI, which calls the Checkout Address Book widget’s
toggleSplitShipping()
method. - The
toggleSplitShipping()
method sets theCartViewModel.isSplitShipping
property to true. - The Checkout Address Book widget shows the Ship To Multiple Addresses UI and hides the Ship To Single Address UI.
Deactivate
In this diagram, the following happens:
- The shopper clicks the Use Single Shipping button on the UI, which calls the Checkout Address Book widget’s
toggleSplitShipping()
method. - The
toggleSplitShipping()
method sets theCartViewModel.isSplitShipping
property to false. - The Checkout Address Book widget shows the Ship To Single Address UI and hides the Ship To Multiple Addresses UI.
Split shipping web form
A split shipping web form allows users to populate shipping options for each cart item. The shippingGroupRelationships
property (which is an observable array) captures the split shipping options for each cart item. Each instance of a ShippingGroupRelationships
object associates a quantity of cart item with a given shipping address and shipping method. A single cart item can have several ShippingGroupRelationships
instances, allowing the cart item to be split across several shipping groups.
Quantity field
The Quantity field allows the shopper to specify the portion of cart item to be associated with a given shipping group. The following code shows a sample UI binding pattern for this feature:
<!-- ko foreach: cart().items -->
<!-- ko foreach: shippingGroupRelationships -->
<input type="number" name="quantity" class="form-control" data-bind="
value: quantity,
event: {change: $parents[1].priceSplitShippingCartForCheckout}">
<!-- /ko -->
<!-- /ko -->
Note: A change of Quantity will cause the widget to make a pricing call, provided the split shipping form is complete and valid.
Shipping Address field
The Shipping Address field allows the shopper to select a shipping address for the specified quantity of the cart item. The following code shows a sample UI binding pattern for this feature:
<!-- ko foreach: cart().items -->
<!-- ko foreach: shippingGroupRelationships -->
<select
class="form-control"
name="shippingAddress"
data-bind="options: $parents[1].user().shippingAddressBook(),
optionsText: $parents[1].getOptionTextForAddress,
value: shippingAddress,
optionsCaption: 'Select shipping address',
event: {change: $parents[1].lookupShippingOptions}">
</select>
<!-- /ko -->
<!-- /ko -->
Shipping Address options should be retrieved from the profile’s shipping address book so that updates to the shipping address book will automatically be reflected in the options list. A change of Shipping Address must trigger a method in your widget that makes an AJAX service call to retrieve the valid shipping options for the selected address and product. The product ID must be passed in this call because some products may have a shipping surcharge and not all shipping methods can be used for products with surcharges.
Shipping Method field
The Shipping Method field allows the shopper to select a shipping method for the specified quantity of the cart item. The following code shows a sample UI binding pattern for this feature:
<!-- ko foreach: cart().items -->
<!-- ko foreach: shippingGroupRelationships -->
<select
class="form-control"
name="shippingMethod"
data-bind="options: shippingOptions,
optionsText: 'displayName',
value: shippingMethod,
optionsCaption: 'Select shipping method',
enable: shippingAddress,
event: {change: $parents[1].priceSplitShippingCartForCheckout}">
</select>
<!-- /ko -->
<!-- /ko -->
The Shipping Method options displayed to the shopper must be populated by a method in your widget that makes an AJAX service call to retrieve the valid shipping options for the selected address. A change of Shipping Address should trigger this method.
A change of Shipping Method should cause the widget to make a pricing call, provided the split shipping form is complete and valid.
Split Items button
The Split Items button creates another shipping group relationship instance for this cart item, allowing the same cart item to be associated with more than one shipping group. The following code shows a sample UI binding pattern for this feature:
<!-- ko foreach: cart().items -->
<!-- ko if: $parent.canAddShippingGroupRelationship($parent) -->
<button class="btn btn-link" data-bind="click: addShippingGroupRelationship">
Split items
</button>
<!-- /ko -->
<!-- /ko -->
It is only possible to split a cart item if the cart item quantity is greater than shippingGroupRelartionships.length
.
Remove Item (X) button
The Remove Item button, shown as an X in the sample UI displayed in this section, removes a shipping group relationship instance. The following code shows a sample UI binding pattern for this feature:
<!-- ko foreach: cart().items -->
<!-- ko foreach: shippingGroupRelationships -->
<button class="btn btn-sm btn-link" data-bind="click:
parents[1].removeShippingGroupRelationship.bind($parent)">
<span class="glyphicon glyphicon-remove"></span>
</button>
<!-- /ko -->
<!-- /ko -->
The removeShippingGroupRelationship
method, used in the click binding above, is a widget method and not the CartItem.removeShippingGroupRelationship
method. It does, however, delegate to CartItem.removeShippingGroupRelationship
, and also makes a pricing call, provided the split shipping form is complete and valid.
Add Address button
The Add Address button opens an address form where a shopper can save a new address to his profile address book. Once created, the new address will automatically appear in the Shipping Address options in the split shipping form. Apart from the inclusion of an alias field, no new address management APIs are required for split shipping. Re-using existing address management functionality is wholly sufficient to for this purpose.
The following illustration shows what an Add Address form might look like with fields for name, address, and phone number information. Note the addition of the Alias field.
ShippingGroupsRelationships
array validation
The shippingGroupRelationships
property has two predefined custom Knockout validators:
- Quantity of item allocated to shipping groups exceeds quantity of item in cart: Checks that the sum of the shipping group quantities is not greater than the cart item quantity.
- Cart item quantity not fully allocated to shipping groups: Checks that the sum of the shipping group quantities is not less than the cart item quantity.
The above validators are computed automatically. The illustration below shows an error message that indicates to the shopper when a validation has failed.
To output the error message on screen, use the validationMessage
binding shown below:
<!-- ko foreach: cart().items -->
<div class="text-danger" data-bind="validationMessage:
shippingGroupRelationships" role="alert"></div> <!-- /ko -->
Price order
The pricing method CartItem.priceCartForCheckout
handles both single and split shipping pricing. There is no change to the API for the split shipping.
As you create your widgets, you should consider when pricing is called. For example, you should call pricing when:
- The Quantity field changes.
- The Shipping Method field changes.
- A shipping group relationship is removed (clicking the X button in the sample UI shown in this section).
The priceCartForCheckout
method uses the isSplitShipping
property to determine which pricing request to make. The priceCartForCheckout
method will only make a pricing request if the split shipping form is complete and valid.
Order summary
Your storefront may need to show a pricing breakdown by shipping group in an Order Summary section, as shown in the following example which displays the shipping group name, subtotal before tax and shipping, shipping costs, sales tax, and total cost for each group.
The following binding pattern outputs the price info per shipping group in the widget shown above.
<!-- ko if: cart().isSplitShipping() -->
<!-- ko foreach: cart().orderShippingGroups -->
<!-- ko if: $data.hasOwnProperty("priceInfo") -->
<div class="well well-sm small"> <strong>
Shipping Group
<span data-bind="text: ($index() + 1)"></span>
(<span data-bind="text: shippingAddress.alias"></span> -
<span data-bind="text:
shippingMethod.shippingMethodDescription"></span>)
</strong> <div class="row">
<div class="col-xs-7">Subtoal</div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.subTotal,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div>
</div> <div class="row">
<div class="col-xs-7">
Shipping (<span data-bind="text:
shippingMethod.shippingMethodDescription"></span>)
</div> <div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.shipping,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div>
</div>
<!-- ko if: $data.hasOwnProperty("discountInfo") -->
<!-- ko if: discountInfo.shippingDiscount !== 0 -->
<div class="row">
<div class="col-xs-7">Shipping Discount </div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: -discountInfo.shippingDiscount,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div>
</div> <!-- /ko -->
<!-- /ko -->
<!-- ko if: priceInfo.shippingSurchargeValue &&
priceInfo.shippingSurchargeValue !== 0 -->
<div class="row">
<div class="col-xs-7">Shipping Surcharge</div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.shippingSurchargeValue,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div>
</div> <!-- /ko -->
<!-- ko if: $parent.cart().showTaxSummary -->
<div class="row">
<div class="col-xs-7">Sales Tax</div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.tax,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div>
</div> <!-- /ko -->
<!-- ko if: (taxPriceInfo.isTaxIncluded &&
$parent.cart().showTaxSummary) -->
<div class="row">
<div class="col-xs-7">Group Total (excluding tax)</div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.totalWithoutTax,
currencyObj: $parent.site().priceListGroup.currency}">
</span> </div>
</div> <!-- /ko -->
<div class="row"> <div class="col-xs-7">
Group Total
<!-- ko if: (taxPriceInfo.isTaxIncluded &&
$parent.cart().showTaxSummary) -->
<span data-bind="widgetLocaleText: 'includingTaxText'"></span>
<!-- /ko --> </div>
<div class="col-xs-5 text-right">
<span data-bind="currency: {
price: priceInfo.total,
currencyObj:
$parent.site().selectedPriceListGroup().currency}">
</span> </div> </div> </div> <!-- /ko --> <!-- /ko -->
<!-- /ko -->
Place order
The OrderViewModel.handlePlaceOrder
method handles placing both single and split shipping orders. There is no change to the API for split shipping. The handlePlaceOrder
method should be called when the Place Order button is clicked.
Order confirmation
Order confirmation should display each shipping group and its relevant information such as the addressee, the shipping method, the items in the shipping group, the subtotal before tax and shipping, shipping costs, sales tax, and a total cost for each group.
The following binding pattern iterates over the shippingGroups
array in the widget shown above.
<!-- ko with: confirmation --> <!-- ko foreach: shippingGroups -->
Mark-up for shipping group here... <!-- /ko -->
<!-- /ko -->
Order details
Order details, like order confirmation, should display each shipping group and its relevant information such as the addressee, the shipping method, the items in the shipping group, the subtotal before tax and shipping, shipping costs, sales tax, and a total cost for each group.
The following binding pattern iterates over the shippingGroups
array in the widget shown above.
<!-- ko with: orderDetails-->
<!-- ko foreach: shippingGroups -->
Mark-up for shipping group here...
<!-- /ko -->
<!-- /ko -->