Introduction

If you want to create any kind of on-line store, you will probably need a shopping cart.

A shopping cart is basically a list that contains products selected by the user while he shops. When the user is finished shopping, he will usually examine the list to double-check that the items, quantities, and prices are correct. If he finds any errors, he should be able to edit the list. Once he is ready, he should be able to check out. The checkout process involves an exchange of information that results in a sale.

Sounds simple, right? And it actually is. The only challenge is performing the checkout, because that involves personal information and money. Fortunately, there are services that handle this type of transaction and you can leverage them. Some of the most popular are PayPal and Google Wallet.

This article describes the implementation of a shopping cart using JavaScript. The cart uses PayPal and Google Wallet payment services. Adding other providers is fairly easy. If you have your own payment infrastructure for example, you can extend the shopping cart to use that in addition to the PayPal and Google Wallet options. Offering more payment options should increase sales.

The article includes a sample application called “Angular Store” that demonstrates how to use the shopping cart in AngularJS applications.

Shopping Cart Requirements

When I started developing the shopping cart, I had the following requirements in mind:

Must be 100% pure JavaScript (so it is easy to integrate into any site)

Must follow the MVVM architecture (so it is easy to customize its look and feel)

Must be safe (we don't want to be responsible for storing people's credit card numbers, etc.)

Must be fast and reliable (we don’t want users to give up before they checkout!)

Must be flexible (it should allow payments to be processed using different services)

Must be extensible (adding new payment methods should be easy)

Must be easy to use (because there's no reason for it to be complicated)

I believe the “ shoppingCart ” class described below addresses all these requirements. It uses jQuery and integrates well with AngularJS applications. The “ shoppingCart ” class contains all the logic and provides the object model needed to create flexible and attractive views.

The Angular Store Sample Application

To understand how the cart works, let’s take a quick look at a typical application. The Angular Store app has three main views:

Store

This is the main view. It presents a list of the products available. Users can search for items using a filter, get detailed information about specific products by clicking their names, add products to the shopping cart, and see a quick summary of what is in their cart. Clicking the summary navigates to the cart. This is what the store view looks like:

Product Details

This view shows details about a product and allows users to add or remove the product to/from the shopping cart. The view also presents a quick summary of the cart so users can tell whether this product is already in the cart. This is what the product details view looks like:

Shopping Cart

This view shows the shopping cart. Users can edit the cart and checkout using PayPal or Google Wallet. Offering more payment options tends to increase sales, because some users may have accounts with one service or the other. This is what the shopping cart view looks like:

AngularJS Infrastructure

The sample application starts with the definition of an AngularJS module that represents the application. The AngularStore module is defined in the app.js file as follows:

var storeApp = angular.module( ' AngularStore' , []). config([ ' $routeProvider' , function ($routeProvider) { $routeProvider. when( ' /store' , { templateUrl: ' partials/store.htm' , controller: storeController }). when( ' /products/:productSku' , { templateUrl: ' partials/product.htm' , controller: storeController }). when( ' /cart' , { templateUrl: ' partials/shoppingCart.htm' , controller: storeController }). otherwise({ redirectTo: ' /store' }); }]);

This first block of code defines the storeApp object that represents the application. It contains a routeProvider that specifies which view should be displayed based on the URL.

For example, when the URL ends with “/cart”, the app should display the view defined in the “partials/shoppingCart.htm” file. The view should be bound to a controller of type “ storeController ”.

When the URL ends with “/product/:productSku”, the app should display the view defined in the “partials/product.htm” file. The view should be bound to a controller of the same type “ storeController ”. In this case, the “/:productSku” represents a variable parameter used to identify the product being shown. It will be replaced at runtime with an actual product code.

In this case, all views have the same type of controller, a class that contains a “ store ” and a “ cart ”.

Because in this case, all views refer to the same store and cart, it makes sense to create these data objects once, at the app level, and allow all controllers to use them. This will improve performance because it eliminates the need to re-load the store and cart items whenever a new view is displayed.

The easiest way to share data between controllers in AngularJS is by defining an app-level “ service ”, and later using this service to initialize the controllers that need them. This link shows a simple example that illustrates the concept.

Here is the definition of the “ DataService ” that provides data shared by all views in the Angular Store application:

storeApp.factory( " DataService" , function () { var myStore = new store(); var myCart = new shoppingCart( " AngularStore" ); myCart.addCheckoutParameters( " PayPal" , " your PayPal merchant account id" ); myCart.addCheckoutParameters( " Google" , " your Google merchant account id " , { ship_method_name_1: " UPS Next Day Air" , ship_method_price_1: " 20.00" , ship_method_currency_1: " USD" , ship_method_name_2: " UPS Ground" , ship_method_price_2: " 15.00" , ship_method_currency_2: " USD" }); return { store: myStore, cart: myCart }; });

The service creates a “ store ” object that contains a list of the products available and a “ shoppingCart ” object that represents the shopping cart.

When the “ shoppingCart ” object is created, it automatically loads its contents from local storage, so users can add items to the cart, close the application, and continue shopping later on.

After creating the cart, the service configures the cart’s checkout parameters. In this example, the cart provides two checkout options:

The “PayPal” option specifies the merchant account to use for checking out, and has no additional options. To use the shopping cart with PayPal, you have to create a merchant account with PayPal. You can do that here. The “Google” option specified the merchant account and additional options related to shipping charges. To use the shopping cart with Google Wallet, you have to create a merchant account with Google. You can do that here.

Once the data service has been created, it can be used by the “ storeController ” objects that will drive all the views in the application. This is done in the controller.js file:

function storeController($scope, $routeParams, DataService) { $scope.store = DataService.store; $scope.cart = DataService.cart; if ($routeParams.productSku != null ) { $scope.product = $scope.store.getProduct($routeParams.productSku); } }

The “ storeController ” function retrieves the store and cart from the “ DataService ” discussed earlier and adds them to the AngularJS $scope object. The $scope object works as a data context for the view.

The app.js and controller.js files contain all the AngularJS part of the application code. The remaining classes (store.js, product.js, and shoppingCart.js) are platform-agnostic.

The ‘store’ and ‘product’ Classes

The ” store ” class is defined in the store.js file.

It exposes a list of products and provides a getProduct method that retrieves an individual product by SKU. This method is used by the “ storeController ” to set the current product when the URL routing specifies a productSku .

function store() { this .products = [ new product( " APL" , " Apple" , " Eat one every…" , 12 , 90 , 0 , 2 , 0 , 1 , 2 ), new product( " AVC" , " Avocado" , " Guacamole…" , 16 , 90 , 0 , 1 , 1 , 1 , 2 ), new product( " BAN" , " Banana" , " These are…" , 4 , 120 , 0 , 2 , 1 , 2 , 2 ), new product( " WML" , " Watermelon" , " Nothing…" , 4 , 90 , 4 , 4 , 0 , 1 , 1 ) ]; this .dvaCaption = [ " Negligible" , " Low" , " Average" , " Good" , " Great" ]; this .dvaRange = [ " below 5%" , " between 5 and 10%" ,… " above 40%" ]; } store.prototype.getProduct = function (sku) { for ( var i = 0 ; i < this .products.length; i++) { if ( this .products[i].sku == sku) return this .products[i]; } return null ; }

The ” product ” class is defined in the product.js file as follows:

function product(sku, name, description, price, cal, carot, vitc, folate, potassium, fiber) { this .sku = sku; this .name = name; this .description = description; this .price = price; this .cal = cal; this .nutrients = { " Carotenoid" : carot, " Vitamin C" : vitc, " Folates" : folate, " Potassium" : potassium, " Fiber" : fiber }; }

The product class has three properties that will be used by the shopping cart: sku (unique ID), name , and price . All other members are used elsewhere within the application, but not by the cart.

Decoupling the cart from the raw product class makes it easier to integrate the cart with existing applications (which often already have product classes generated automatically from databases).

The ‘shoppingCart’ Class

The “ shoppingCart ” class is the most interesting class in the project. It is defined in the shoppingCart.js file and implements an object model as follows:

shoppingCart(cartName)

This is the constructor.

The cartName parameter identifies the cart when saving it to or loading it from local storage. Before you can actually use the cart for checkout operations, you must initialize it by adding one or more payment providers. This is done with the addCheckoutParameters method.

addCheckoutParameters(serviceName, merchantID, [options])

This method defines a set of checkout parameters.

The serviceName parameter defines the name of the payment provider to use. In the current implementation, this must be set to either “PayPal” or “Google”.

The merchantID parameter specifies the merchant account associated with the service. You can create PayPal and Google merchant accounts using these links:

The options parameter defines additional provider-specific fields. In our example, we used this parameter to specify custom shipping methods associated with the Google checkout. Both PayPal and Google support a large number of optional parameters that you can use to customize the checkout process.

addItem(sku, name, price, quantity)

This method adds or removes items from the cart.

If the cart already contains items with the given sku , then the quantity of that item is modified. If the quantity reaches zero, the item is automatically removed from the cart.

If the cart does not contain items with the given sku , then a new item is created and added to the cart using the specified sku , name , price , and quantity .

After the cart has been updated, it is automatically saved to local storage.

clearItems()

This method clears the cart by removing all items. It also saves the empty cart to local storage.

getTotalCount([sku])

This method gets the quantity of items or a given type or for all items in the cart.

If the sku is provided, then the method returns the quantity of items with that sku . It the sku is omitted, then the method returns the quantity of all items in the cart.

getTotalPrice([sku])

This method gets the total price (unit price * quantity) for one or all items in the cart.

If the sku is provided, then the method returns the price of items with that sku . It the sku is omitted, then the method returns the total price of all items in the cart.

checkout([serviceName], [clearCart])

This method initiates a checkout transaction by building a form object and submitting it to the specified payment provider.

If provided, the serviceName parameter must match one of the service names registered with calls to the addCheckoutParameters method. If omitted, the cart will use the first payment service registered. The clearCart parameter specifies whether the cart should be cleared after the checkout transaction is submitted.

The checkout method is the most interesting in this class, and is listed below:

shoppingCart.prototype.checkout = function (serviceName, clearCart) { if (serviceName == null ) { var p = this .checkoutParameters[Object.keys( this .checkoutParameters)[ 0 ]]; serviceName = p.serviceName; } if (serviceName == null ) { throw " Define at least one checkout service." ; } var parms = this .checkoutParameters[serviceName]; if (parms == null ) { throw " Cannot get checkout parameters for '" + serviceName + " '." ; } switch (parms.serviceName) { case " PayPal" : this .checkoutPayPal(parms, clearCart); break ; case " Google" : this .checkoutGoogle(parms, clearCart); break ; default : throw " Unknown checkout service: " + parms.serviceName; } }

The method starts by making sure it has a valid payment service, and then defers the actual work to the checkoutPayPal or checkoutGoogle methods. These methods are very similar but are service-specific. The checkoutPayPal method is implemented as follows:

shoppingCart.prototype.checkoutPayPal = function (parms, clearCart) { var data = { cmd: " _cart" , business: parms.merchantID, upload: " 1" , rm: " 2" , charset: " utf-8" }; for ( var i = 0 ; i < this .items.length; i++) { var item = this .items[i]; var ctr = i + 1 ; data[ " item_number_" + ctr] = item.sku; data[ " item_name_" + ctr] = item.name; data[ " quantity_" + ctr] = item.quantity; data[ " amount_" + ctr] = item.price.toFixed( 2 ); } var form = $( ' <form></form>' ); form.attr( " action" , " https://www.paypal.com/cgi-bin/webscr" ); form.attr( " method" , " POST" ); form.attr( " style" , " display:none;" ); this .addFormFields(form, data); this .addFormFields(form, parms.options); $( " body" ).append(form); this .clearCart = clearCart == null || clearCart; form.submit(); form.remove(); }

The checkoutPayPal method builds a form, populates it with hidden input fields that contain the cart data, and submits the form to the PayPal servers. The whole process is described here.

The checkoutGoogle method is very similar. It also builds and submits a form, the only difference being the name and content of the fields. Details are available here.

Both checkout methods allow you to add custom fields specified in the options parameter of the cart’s addCheckoutParameters method. These custom fields can be used to specify things like return URLs, custom images for the cart on the server’s site, custom shipping rules and prices, etc.

When the checkout method submits the form, the user is taken to the appropriate site (PayPal or Google Wallet), where he can review the information about the items, update his own personal and credit card information, and finalize the transaction. All this happens outside the scope of the application. The payment provider will then use the information associated with the merchant id provided by the form to notify you of the transaction so you can collect the payment and ship the goods to the customer.

If you wanted to add more payment options to the cart, you would have to:

Modify the addCheckoutParameters method to accept the new service name. Create a new checkout<ServiceName> method to handle the checkouts using the new service. This would probably be similar to the existing checkoutPayPal and checkoutGoogle methods. Modify the checkout method to call the new method depending on the service name specified by the user.

For example, if you wanted to leverage an existing payment infrastructure you have on your site, you could create a method similar to checkoutPayPal , but with a URL on your site. The server would receive the form with all the information encoded as hidden fields, and would have access to the current session, user, etc. At this point, you would have all the information required by your payment infrastructure (cart and user).

AngularJS Views

Now that we have covered the AngularJS infrastructure and the controller classes, let’s turn our attention to the views.

The default.htm file contains the master view. It is implemented as follows:

< !doctype html > < html ng-app =" AngularStore" > < head > <!-- <!-- <!-- < script src =" js/product.js" type =" text/javascript" > < / script > < script src =" js/store.js" type =" text/javascript" > < / script > < script src =" js/shoppingCart.js" type =" text/javascript" > < / script > < script src =" js/app.js" type =" text/javascript" > < / script > < script src =" js/controller.js" type =" text/javascript" > < / script > < link href =" css/style.css" rel =" stylesheet" type =" text/css" / > < /head > < body > < div class =" container-fluid" > < div class =" row-fluid" > < div class =" span10 offset1" > < h1 class =" well" > < a href =" default.htm" > < img src =" img/logo.png" height =" 60" width =" 60" alt =" logo" / > < /a > Angular Store < /h1 > < div ng-view > < /div > < /div > < /div > < /div > < /body > < /html >

Notice the following important points:

The “ ng-app ” attribute associates the page with the AngularStore module defined in the app.js file. This attribute takes care of the URL routing, view injection, and providing each view with the appropriate controllers. The “ ng-view ” div marks the place where AngularJS will inject the partial pages that correspond to the routed views. Recall that our application has three partial pages: store.htm, product.htm, and shoppingCart.htm. The parts of the page around the “ ng-view ” div remain in place as you switch views, acting as a master page. In this sample, this area shows the app logo and a title. The sample application uses Bootstrap, twitter’s public framework that includes powerful and easy to use css styles. Bootstrap makes it easy to create adaptive layouts that work well on the desktop and on mobile devices (for details, see http://twitter.github.io/bootstrap/).

The store.htm partial view is implemented as follows:

< p class =" text-info" > Welcome to the Angular Store < br / > Please select the products you want …. < br / > < /p > < p > Search: < input ng-model =" search" > < /p > < table class =" table table-bordered" > < tr class =" well" > < td class =" tdRight" colspan =" 4" > < a href =" default.htm#/cart" title =" go to shopping cart" ng-disabled =" cart.getTotalCount() < 1" > < i class =" icon-shopping-cart" / > < b > {{cart.getTotalCount()}} < /b > items, < b > {{cart.getTotalPrice() | currency}} < /b > < /a > < /td > < /tr > < tr ng-repeat =" product in store.products | orderBy:'name' | filter:search" > < td class =" tdCenter" > < img ng-src =" img/products/{{product.sku}}.jpg" alt =" {{product.name}}" / > < /td > < td > < a href =" #/products/{{product.sku}}" > < b > {{product.name}} < /b > < /a > < br / > {{product.description}} < /td > < td class =" tdRight" > {{product.price | currency}} < /td > < td class =" tdCenter" > < a href =" " ng-click =" cart.addItem(product.sku, product.name, product.price, 1)" > add to cart < /a > < /td > < /tr > < tr class =" well" > < td class =" tdRight" colspan =" 4" > < a href =" default.htm#/cart" title =" go to shopping cart" ng-disabled =" cart.getTotalCount() < 1" > < i class =" icon-shopping-cart" / > < b > {{cart.getTotalCount()}} < /b > items, < b > {{cart.getTotalPrice() | currency}} < /b > < /a > < /td > < /tr > < /table >

The view consists of a table with three regions: the first row contains a single cell that spans the entire table and shows a summary of the shopping cart. Notice how it uses the getTotalCount and getTotalPrice methods to retrieve the cart information. Clicking this element redirects the browser to “ default.htm#/cart ”, which shows the shopping cart.

The view uses Bootstrap’s built-in icons, in this case the “ icon-shopping-cart ” class to enhance the view with simple and attractive icons. Bootstrap includes a set of 140 icons that cover a lot of common scenarios (see the complete list here).

The body of the table uses an ng-repeat attribute to show a sorted, filtered list of all products. Each product row contains an image, a description that is also a link to the product details view, the product price, and a link that adds the product to the shopping cart. Adding items to the cart is accomplished by using the “ ng-click ” attribute to invoke the cart’s addItem method.

The “ orderBy ” and “ filter ” clauses are filters provided by AngularJS. You can learn more about AngularJS filters here.

The last row is a copy of the first. It shows another summary of the cart below the product list, making navigation easier in stores that have a lot of products.

The product.htm partial view is very similar, so we will not list it here.

The most interesting partial view is the shopping cart itself, in shoppingCart.htm:

< p class =" text-info" > Thanks for shopping at the Angular Store. < br / > This is your shopping cart. Here you can edit the items, go back to the store, clear the cart, or check out. < /p > < div class =" container-fluid" > < div class =" row-fluid" >

The first part of the view shows a title and sets up a Bootstrap “ fluid-row ” div that will show two items: the cart items on the left and the cart buttons on the right.

<!-- < div class =" span8" > < table class =" table table-bordered" > <!-- < tr class =" well" > < td > < b > Item < /b > < /td > < td class =" tdCenter" > < b > Quantity < /b > < /td > < td class =" tdRight" > < b > Price < /b > < /td > < td / > < /tr > <!-- < tr ng-hide =" cart.getTotalCount() > 0" > < td class =" tdCenter" colspan =" 4" > Your cart is empty. < /td > < /tr > <!-- < tr ng-repeat =" item in cart.items | orderBy:'name'" > < td > {{item.name}} < /td > < td class =" tdCenter" > < div class =" input-append" > <!-- < input class =" span3 text-center" type =" tel" ng-model =" item.quantity" ng-change =" cart.saveItems()" / > < button class =" btn btn-success" type =" button" ng-disabled =" item.quantity >= 1000" ng-click =" cart.addItem(item.sku, item.name, item.price, +1)" > + < /button > < button class =" btn btn-inverse" type =" button" ng-disabled =" item.quantity <= 1" ng-click =" cart.addItem(item.sku, item.name, item.price, -1)" > - < /button > < /div > < /td > < td class =" tdRight" > {{item.price * item.quantity | currency}} < /td > < td class =" tdCenter" title =" remove from cart" > < a href =" " ng-click =" cart.addItem(item.sku, item.name, item.price, -10000000)" > < i class =" icon-remove" / > < /a > < /td > < /tr > <!-- < tr class =" well" > < td > < b > Total < /b > < /td > < td class =" tdCenter" > < b > {{cart.getTotalCount()}} < /b > < /td > < td class =" tdRight" > < b > {{cart.getTotalPrice() | currency}} < /b > < /td > < td / > < /tr > < /table > < /div >

The items are shown in a “ span8 ” div . Bootstrap layouts are based on 12 width units, so this div will be approximately two-thirds of the width available.

The table that contains the cart items starts with a header row, followed by an empty cart indicator. The “ ng-hide ” attribute is used to ensure the indicator is visible only when the cart is empty.

The body of the table is generated with an “ ng-repeat ” attribute that loops through the items in the cart.items array. For each item, the table shows the item name, followed the item quantity and price.

The item quantity is shown using a composite element made up of an input field bound to the item.quantity property and two buttons used to increment or decrement the quantity.

Notice how the “ ng-change ” attribute is used to save the cart contents when the quantity changes. Notice also how the decrement button is disabled when the item quantity reaches one. At this point, decrementing the quantity would remove the item from the cart, and we don’t want users to do that by accident.

After the quantity field, the table shows the total price of the item (unit price times quantity) and a button that allows users to remove the item from the cart.

The table footer shows a summary of the cart contents, and is automatically updated as the user edits quantities or removes items from the cart. The updates are handled automatically by AngularJS.

In addition to the cart items, the view has a section with buttons used to return to the store, to clear the cart, and to check out:

<!-- < div class =" span4" > < p class =" text-info" > < button class =" btn btn-block" onclick =" window.location.href='default.htm'" > < i class =" icon-chevron-left" / > back to store < /button > < button class =" btn btn-block btn-danger" ng-click =" cart.clearItems()" ng-disabled =" cart.getTotalCount() < 1" > < i class =" icon-trash icon-white" / > clear cart < /button > < /p >

The section starts with a “ span4 ” div which fills up the page (remember the items were placed in a “ span8 ” div ).

The “back to store” button navigates back to the “default.htm” page, which maps to the store.

The “clear cart” button invokes the cart’s clearItems method, and is enabled only if the cart is not already empty.

< p class =" text-info" > < button class =" btn btn-block btn-primary" ng-click =" cart.checkout('PayPal')" ng-disabled =" cart.getTotalCount() < 1" > < i class =" icon-ok icon-white" / > check out using PayPal < /button > < button class =" btn btn-block btn-primary" ng-click =" cart.checkout('Google')" ng-disabled =" cart.getTotalCount() < 1" > < i class =" icon-ok icon-white" / > check out using Google < /button > < /p >

The checkout buttons call the cart’s checkout method passing in the appropriate service name. Remember we configured the cart in the app.js file to accept PayPal and Google as valid payment service providers.

< p class =" text-info" > < button class =" btn btn-block btn-link" ng-click =" cart.checkout('PayPal')" ng-disabled =" cart.getTotalCount() < 1" > < img src = https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif alt =" checkout PayPal" / > < /button > < button class =" btn btn-block btn-link" ng-click =" cart.checkout('Google')" ng-disabled =" cart.getTotalCount() < 1" > < img src = https://checkout.google.com/buttons/checkout.gif?... alt =" checkoutGoogle" / > < /button > < /p >

These buttons provide the same cart checkout services, but use images provided by PayPal and Google. Personally, I think the provider buttons may look a little less consistent on the page, but provide a familiar feeling to the user.

The nice thing about Bootstrap’s layout mechanism is that it is ‘adaptive’. If you view the page on mobile devices, the layout automatically adapts to the screen width. The screenshots below illustrate this. The image on the left shows a wide view, with buttons on the right of the items (typical desktop view). The image on the right shows a narrow view, with buttons below the items (typical mobile view).

Conclusion

The “ shoppingCart ” class presented here fulfills the requirements outlined in the beginning of the article. It is 100% JavaScript, and has no requirements on the server, so it should be easy to add to existing projects. The cart supports PayPal and Google Wallet, which are popular payment services. Many applications will probably want to extend this to support their own custom payment services, and that should be easy to do.

The MVVM pattern allows the same cart object to be exposed in multiple views, which contain very simple markup and virtually no logic. The sample application for example has a view that shows the whole cart, and allows users to edit it; but it also shows cart summaries on the store and product pages. These views are easy to create and customize, and there is no impact on the application logic.

I am a big fan of AngularJS. In addition to the MVVM support it provides, which is great, it has an amazing list of features that include routing and partial views, filters, custom directives, and more.

I especially like the fact that AngularJS’s data binding features work with plain JavaScript objects. Some MVVM libraries (like KnockoutJS) require special “observable” properties, which are declared and accessed using a syntax that is different from plain properties.

The one aspect of AngularJS I do not like is the lack of documentation. You can find a lot of information about the details of pretty much any aspect of AngularJS, but I have not found a good reference that presents an overall conceptual view of the framework. My favorite source of documentation on AngularJS is a series of videos created by John Lindquist which you can find here.

I also like Bootstrap, because it makes it easy to create attractive, responsive HTML layouts. In addition to a nice set of styles and icons, Bootstrap also provides some JavaScript components that you can use to enhance your UIs with things like tooltips, pop-overs, menus, etc. You can learn about Bootstrap here.

Github Version

A little while ago, a reader asked me to post this project on github, so he and others could fork it and add new features including payment processors in addition to PayPal and Google Wallet. The reader was interested in adding support for Stripe.js, a payment processor developed specifically for developers.

I loved the idea, but I wasn't fast enough, so he beat me to it. The project is now available on github, including the added support for Stripe.js, all courtesy of Mr. Spike! So thanks Spike, and for those interested in this new and improved version of the cart, here's the link:

References