AngularJS Responsive Video Shopping Cart
by Bill SerGio
Introduction
There are a lot of people who are not computer or Internet savy but who need a simple shopping cart that doesn't require a database or a programmer to install it. I recall a customer calling me up once for tech support for one of my software programs and telling me "Your software doesn't work!" I asked what was the problem and he stated that he was stepping on the "foot pedal" and nothing was happening. It turned out that he had put the mouse on the floor and thought you had to pump it with your foot. Needles to say it was a challenge to help him.
In this article I will show you how to write an
eas-to-install AngularJS Responsive Shopping Cart with Video
that includes practical features like an option to display a product image or a
video of each item, importing products from a separate JSON text file instead of
hard-coding product data in the code itself, Google analytics, additional
merchant gateway options, responsiveness so it looks good on mobile devices, a
folder layout that would allows simply dropping it onto the root level of any
existing website, cool-looking naigation like a boostrap menu, etc.
Shopping Cart Features
Here are some of the practical features I included:
- Must be responsive so it will display and scroll perfectly on any mobile device.
- Must have a cool-looking, gradient-colored responsive Bootstrap Menu.
- Allow Multiple Stores in our cart.
- Must read the products and their descriptions from a JSON text file.
- Must be able to play a Video (TV Commercial) about a product in addition to a picture of the product.
- Must at least include merchant gateways for PayPal, Google Wallet, and Stripe.
- Must be extensible so that adding new features like payment methods is easy.
- Must allow FREE products that can't be added to the cart.
- Must handle Google Analytics using AngularJS.
- Must include a Dialog Popup using AngularJS to display additional information or a video.
- Must be able to include links to thrird-party websites like Google Play, etc.
- Must have a directory structure that allows it to be "dropped" at the root level onto any existing website.
- Must display text as HTML so it attracts the potential customer visually.
- Place icons INSIDE buttons for better UI/UX
- Must follow MVVM architecture.
- Must include Pagination to control number of products displayed per page
AngularJS App Structure
The sample application starts with the definition of an AngularJS module that represents the application. The module AngularStore module is defined in the app.js file passes in two dependcies, namely ['ui.bootstrap', 'favicon']. I decide to use "ui.bootstrap" in a minimal way as a dependency to "AngularStore" for the menu and part of the UI look I wanted. And I added favicon to help to add images from websites to the menu. In addition, I also added jQuery to simply demonstrate how to integrate it with an AngularJS application. The “shoppingCart” class contains all the logic and provides the object model needed to create fully responsive and attractive views.
'use strict'; // App Module: the name AngularStore matches the ng-app attribute in the main tag // the route provides parses the URL and injects the appropriate partial page var storeApp = angular.module('AngularStore', ['ui.bootstrap', 'favicon']) .config(['$routeProvider', function($routeProvider) { $routeProvider. when('/store', { templateUrl: '/ac_partials/store.htm', controller: storeController }). when('/ac_products/:productSku', { templateUrl: '/ac_partials/product.htm', controller: storeController }). when('/cart', { templateUrl: '/ac_partials/cart.htm', controller: storeController }). otherwise({ redirectTo: '/store' }); }]);
The first thing you will notice is that I prefaced each of our AmgularJS folders with "ac_" so that when we drop the cart into an existing website on a shared server at the root level. Some shared hosting accounts do not allow creating a separate virtual directory so the cart can now be added at the root level and our cart files and folders will not conflict with existing folders or files.
We have a routeProvider that
specifies which view should be displayed based on the URL.
For example, when the URL ends with “/cart”, the app should
load the view defined in the "ac_partials/cart.htm” file. And we will bind
all of our views to a controller “storeController,” a
class that contains a “store” and a “cart”.
The easiest way to share data between controllers in AngularJS
is by defining an app-level “service” to initialize the controllers that need
them. We will create a data service that provides a store and a shopping cart
that will be shared by all views instead of creating fresh ones for each view to improve performance
by eliminating the need to re-load the store and cart items each time a new view is
displayed.
We want our “DataService” to retrieve our sotre products data from a JSON text
file. Here is the definition of the “DataService” that provides data shared by
all views in the Angular Store application.
Our DataService will load data from a json file asynchronously so we will need to use promise and deferred.
A promise in Angular.js act as an placeholder
from where a javascript object returns some result as data which is done in an
asynchronous way and it does not guarantee any fixed response time. This deferred
object is constructed with $q.defer().
This Api is used to notify the success or unsuccesful completion of the
asynchronous work, which is within the context of Deferred Api. After completing
the task in deferred
object,
we can have access to the result in promise
object.
// create a data service that provides a store and a shopping cart that // will be shared by all views (instead of creating fresh ones for each view). storeApp.factory('DataService', function ($http, $q) { function Store() { var productsDeferred = $q.defer(); this.products = productsDeferred.promise; $http.get('/ac_products/products.txt').success(function (data) { var products = []; for (var i = 0, len = data.length; i < len; i++) { var prod = data[i]; if (prod.storeid == "7cc6cb94-0938-4675-b84e-6b97ada53978") { products.push(prod); } } productsDeferred.resolve(products); }); } Store.prototype.getProduct = function (sku) { return this.products.then(function (products) { // MUST use products, it's the real value; this.products is a promise for (var i = 0; i < products.length; i++) { if (products[i].sku == sku) return products[i]; } return null; }); }; Store.prototype.getProducts = function () { return this.products.then(function (products) { return products; }); }; // create store var myStore = new Store(); // create shopping cart var myCart = new shoppingCart("AngularStore"); // enable PayPal checkout // note: the second parameter identifies the merchant; in order to use the // shopping cart with PayPal, you have to create a merchant account with // PayPal. You can do that here: // https://www.paypal.com/webapps/mpp/merchant //myCart.addCheckoutParameters("PayPal", "paypaluser@youremail.com"); myCart.addCheckoutParameters("PayPal", "tvmogul1@yahoo.com"); // enable Google Wallet checkout // note: the second parameter identifies the merchant; in order to use the // shopping cart with Google Wallet, you have to create a merchant account with // Google. You can do that here: // https://developers.google.com/commerce/wallet/digital/training/getting-started/merchant-setup myCart.addCheckoutParameters("Google", "GooGle_Wallet_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" } ); // enable Stripe checkout // note: the second parameter identifies your publishable key; in order to use the // shopping cart with Stripe, you have to create a merchant account with // Stripe. You can do that here: // https://manage.stripe.com/register myCart.addCheckoutParameters("Stripe", "pk_test_stripe", { chargeurl: "https://localhost:1234/processStripe.aspx" } ); // return data object with store and cart return { store: myStore, cart: myCart }; });
Our Angular Views: Store, Product, and Cart
Our responsive Angular Store App has three main views:
Store View: This is the first view that is loaded when the app runs showing the products available. Users can search for items using a filter, and obtain detailed information about specific products by watching the product's TV commercial (i.e., video) if the product has one, or by clicking the product name. Users can also add products to the shopping cart if they have a price or obtain a free sample of a product if a product has a zero cost. Users can also view a summary of what is in their cart by clicking the summary which navigates to the cart. Shown below are what the responsive store view looks like on both a laptop and on a mobile device.
Product View: This view shows more details about a product and also allows users to add or remove the product in/from the shopping cart and shows how many of the product are added to the cart. You can display a video of the product or an image. If an image of the product is displayed then clicking on the image will popup a dialog showing a larger view of the image. You can see below what the Product View looks like with an image displayed.
Cart View: This view shows the shopping cart. Users can edit the cart and checkout using PayPal, Google Wallet, and stripe. Check my website in the next week and I will also add a Bit Coin Payment option as well. Offering more payment options increases sales by boosting the seller's credibility. Below is what the Cart View looks like on a laptop.
The service reads our "products.txt" JSON file of products and creates a “store” object that containing the products available and a “shoppingCart” object that automatically loads its contents from local storage. The cart provides three checkout methods:
- PayPal. Thispayment method specifies the merchant account or BuyNow account(not a merchant account) to use for payment. To use PayPal, you have to create either a BuyNow Account or a merchant account with PayPal. You can do that here: https://www.paypal.com/webapps/mpp/merchantaypal.com/webapps/mpp/merchant
- Google Wallet. This payment method requires that you create a merchant account with Google. You can do that here: https://developers.google.com/commerce/wallet/digital/training/getting-started/merchant-setup
- Stripe. This payment method allows you to embed their API on a websites to accept payments, without the need of getting a merchant account. Stripe has no setup fees, monthly fees, minimum charges, validation fees, card storage fees, or charges for failed payments. Stripe has a 7-day waiting period for transactions to be completed so that Stripe can profile the businesses involved and detect fraud. https://stripe.com
Our DataService will be used by the storeController to display the various views in the application. The storeController retrieves the store and cart from the DataService and adds them to the AngularJS $scope object which functions as a data context for the views. The storeController is where we can set the currentPage, the number of products per page and the maximum number of products used for our Pagination.
// the storeController contains two objects: // store: contains the product list // cart: the shopping cart object function storeController($scope, $routeParams, DataService) { $scope.filteredProducts = []; $scope.currentPage = 1; $scope.numPerPage = 10; $scope.maxSize = 25; // get store and cart from service $scope.store = DataService.store; $scope.cart = DataService.cart; $scope.products = []; // use routing to pick the selected product if ($routeParams.productSku != null) { $scope.product = $scope.store.getProduct($routeParams.productSku); } DataService.store.getProducts().then(function (data) { // Executes when AJAX call completes $scope.products = data; $scope.numPages = function () { return Math.ceil($scope.products.length / $scope.numPerPage); }; $scope.$watch('currentPage + numPerPage', function () { var begin = (($scope.currentPage - 1) * $scope.numPerPage); var end = begin + $scope.numPerPage; $scope.filteredProducts = $scope.products.slice(begin, end); }); }); }
The JSON 'Products.txt' File
I decided to use a JSON format to storte the products and their properties and retrieve them using AJAX as shown below.
[ { "productid": "7D6A083B-01C4-4E74-9F10-2916543188B8", "sku": "WildWorkout", "productname": "WildWorkout®", "storeid": "7cc6cb94-0938-4675-b84e-6b97ada53978", "categoryname": "Android", "header": "Exercises based on the principles of how the wild animals stay in shape", "shortdesc": "Exercises based on the principles of how the wild animals stay in shape. In the Wild Workout® Mobile App we selected wild animals with massive strength in certain areas of their bodies to develop a total body workout of 45 muscle building, fat burning, body shaping exercises like no other that will have the jaws of others dropping in disbelief and envy.", "description": "Exercises based on the principles of how the wild animals stay in shape. In the Wild Workout® Mobile App we selected wild animals with massive strength in certain areas of their bodies to develop a total body workout of 45 muscle building, fat burning, body shaping exercises like no other that will have the jaws of others dropping in disbelief and envy.", "link": "https://play.google.com/store/apps/details?id=com.sergioapps.wildworkout", "imageurl": "", "imagename": "wildworkout.png", "tube": "youtube", "videoid": "YyZNIarRYSc", "showvideo": true, "unitprice": 0.00, "saleprice": 0, "unitsinstock": 22, "unitsonorder": 0, "reorderlevel": 0, "expecteddate": null, "discontinued": null, "notes": "", "faux": null, "sortorder": 1 }, ...
The 'shoppingCart' class
The shoppingCart class implements the object model, i.e., shoppingCart(cartName), with a cartName parameter that identifies the cart when saving it to or loading it from local storage and exposes a number of essential methods.
addCheckoutParameters(serviceName, merchantID, [options])
The addCheckoutParameters(serviceName, merchantID, [options]) method initializes the cart by adding one or more payment providers using the that requires two parameters. The serviceName parameter is the payment provider to use. The merchantID parameter is the merchant account or gateway associated with the service. 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)
The additem(sku, name, price, quantity) 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 is increased or decresed by one. The item is automatically removed from the cart if the quantity reaches zero. 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()
The clearItems() method clears the cart by removing all items and saves the empty cart to local storage.
getTotalCount(sku)
The getTotalCount(sku) 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)
The getTotalPrice(sku) 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)
The checkout(serviceName, clearCart) 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:
// check out shoppingCart.prototype.checkout = function (serviceName, clearCart) { // select serviceName if we have to if (serviceName == null) { var p = this.checkoutParameters[Object.keys(this.checkoutParameters)[0]]; serviceName = p.serviceName; } // sanity if (serviceName == null) { throw "Use the 'addCheckoutParameters' method to define at least one checkout service."; } // go to work 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; case "Stripe": this.checkoutStripe(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:
// check out using PayPal; for details see:
// http://www.paypal.com/cgi-bin/webscr?cmd=p/pdn/howto_checkout-outside
// check out using PayPal for details see:
// www.paypal.com/cgi-bin/webscr?cmd=p/pdn/howto_checkout-outside
shoppingCart.prototype.checkoutPayPal = function (parms, clearCart) {
// global data
var data = {
cmd: "_cart",
business: parms.merchantID,
upload: "1",
rm: "2",
charset: "utf-8"
};
// item data
for (var i = 0; i < this.items.length; i++) {
var item = this.items[i];
var ctr = i + 1;
data["item_number_" + ctr] = item.sku;
var z1 = item.productname;
var z2 = z1.replace('™', '™'); //™ = TM
var z3 = z2.replace('℠', '℠'); //℠ = SM
var z4 = z3.replace('®', '®'); //® = Registered
var z5 = z4.replace('©', '©'); //© = Copyright
var z6 = z5.replace('℗', '℗'); //℗ = Patent
data["item_name_" + ctr] = z6;
data["quantity_" + ctr] = item.quantity;
data["amount_" + ctr] = item.unitprice.toFixed(2);
}
// build form
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);
// submit form
this.clearCart = clearCart == null || clearCart;
form.submit();
form.remove();
}
The shoppingCart.prototype.checkoutPayPal = function (parms, clearCart) method builds a form, populates it
with hidden input fields that contain the cart data, and submits the form to the PayPal servers.
See: https://www.paypal.com/cgi-bin/webscr?cmd=p/pdn/howto_checkout-outside
The
shoppingCart.prototype.checkoutGoogle = function (parms, clearCart)
method is very similar. It also
builds and submits a form, the only difference being the name and content of
the fields.
See:
https://developers.google.com/checkout/developer/Google_Checkout_Custom_Cart_How_To_HTML.
The shoppingCart.prototype.checkoutStripe = function (parms, clearCart) Cart) method also builds and submits a form, the only difference being the name and content of the fields. See: https://stripe.com/docs/checkout
All of these checkout methods allow you to add custom fields specified in the optionsptions 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 storefront.html file contains the master view implemented as follows:
<!doctype html> <html ng-app="AngularStore"> <head> <!-- includes for jQuery, Angular, and Bootstrap --> <!-- … --> <!-- includes for the Angular Store app --> <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: http://twitter.github.io/bootstrap/base-css.html#icons).
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: http://egghead.io/video/rough-draft-angularjs-built-in-filters/
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.
<!-- items -->
<div class="span8">
<table class="table table-bordered">
<!-- header -->
<tr class="well">
<td><b>Item</b></td>
<td class="tdCenter"><b>Quantity</b></td>
<td class="tdRight"><b>Price</b></td>
<td />
</tr>
<!-- empty cart message -->
<tr ng-hide="cart.getTotalCount() > 0" >
<td class="tdCenter" colspan="4">Your cart is empty. </td>
</tr>
<!-- cart items -->
<tr ng-repeat="item in cart.items | orderBy:'name'">
<td>{{item.name}}</td>
<td class="tdCenter">
<div class="input-append">
<!-- use type=tel instead of number to prevent spinners -->
<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>
<!-- footer -->
<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:
<!-- buttons -->
<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 provides 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 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 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.
I can recommend a series of videos on AngularJS created by John Lindquist which you can find here: http://www.youtube.com/user/johnlindquist.
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: http://twitter.github.io/bootstrap/.