SportsStore – Navigation and Checkout with AngularJS: Creating the Cart

The user can see the products that I have available, but I can’t sell anything without a shopping cart. In this section, I will build the cart functionality that will be familiar to anyone who has used an e-commerce site, the basic flow of which is illustrated by Figure 7-3.

As you will see in the following sections, several sets of changes are required to implement the cart feature, including creating a custom AngularJS component.

1. Defining the Cart Module and Service

So far, I have been organizing the files in my project based on the type of component they contain: Filters are defined in the filters folder, views in the views folder, and so on. This makes sense when building the basic features of an application, but there will always be some functionality in a project that is relatively self-contained but requires a mix of AngularJS components. You can continue to organize the files by component type, but I find it more useful to order the files by the function that they collectively represent, for which I use the components folder. The cart functionality is suitable for this kind of organization because, as you will see, I am going to need partial views and several components to get the effect I require. I started by creating the components/cart folder and adding a new JavaScript file to it called cart.js. You can see the contents of this file in Listing 7-5.

Listing 7-5. The Contents of the cart.js File

angular.module(“cart”, [])

.factory(“cart”, function () {

var cartData = [];

return {

addProduct: function (id, name, price) {

var addedToExistingItem = false;

for (var i = 0; i < cartData.length; i++) {

if (cartData[i].id == id) {

cartData[i].count++;

addedToExistingItem = true;

break;

}

}

if (!addedToExistingItem) {

cartData.push({

count: 1, id: id, price: price, name: name

});

}

},

removeProduct: function (id) {

for (var i = 0; i < cartData.length; i++) {

if (cartData[i].id == id) {

cartData.splice(i, 1);

break;

}

}

},

getProducts: function () {

return cartData;

}

}

});

I started by creating a custom service in a new module called cart. AngularJS provides a lot of its functionality through services, but they are simply singleton objects that are accessible throughout an application. (Singleton just means that only one object will be created and shared by all of the components that depend on the service.)

Not only does using a service allow me to demonstrate an important AngularJS feature, but implementing the cart this way works well because having a shared instance ensures that every component can access the cart and have the same view of the user’s product selections.

As I explain in Chapter 18, there are different ways to create services depending on what you are trying to achieve. I have used the simplest in Listing 7-5, which is to call the Module.factory method and pass in the name of the service (which is cart, in this case) and a factory function. The factory function will be invoked when AngularJS needs the service and is responsible for creating the service object; since one service object is used throughout the application, the factory function will be called only once.

My cart service factory function returns an object with three methods that operate on a data array that is not exposed directly through the service, which I did to demonstrate that you don’t have to expose all of the workings in a service. The cart service object defines the three methods described in Table 7-1. I represent products in the cart with objects that define id, name, and price properties to describe the product and a count property to record the number the user has added to the basket.

2. Creating a Cart Widget

My next step is to create a widget that will summarize the contents of the cart and provide the user with the means to begin the checkout process, which I am going to do by creating a custom directive. Directives are self-contained, reusable units of functionality that sit at the heart of AngularJS development. As you start with AngularJS, you will rely on the many built-in directives (which I describe in Chapters 9-12), but as you gain confidence, you will find yourself creating custom directives to tailor functionality to suit your applications.

You can do a lot with directives, which is why it takes me six chapters to describe them fully later in the book. They even support a cut-down version of jQuery, called jqLite, to manipulate elements in the DOM. In short, directives allow you to write anything from simple helpers to complex features and to decide whether the result is tightly woven into the current application or completely reusable in other applications. Listing 7-6 shows the additions I made to the cart.js file to create the widget directive, which is at the simpler end of what you can do with directives.

Listing 7-6. Adding a Directive to the cart.js File

angular.module(“cart”, [])

.factory(“cart”, function () {

var cartData = [];

return {

// …service statements omitted for brevity…

}

})

.directive(“cartSummary”, function (cart) {

return {

restrict: “E”,

templateUrl: “components/cart/cartSummary.html”,

controller: function ($scope) {

var cartData = cart.getProducts();

$scope.total = function () {

var total = 0;

fox (var i = 0; i < cartData.length; i++) {

total += (cartData[i].price * cartData[i].count);

}

return total;

}

$scope.itemCount = function () {

var total = 0;

for (var i = 0; i < cartData.length; i++) {

total += cartData[i].count;

}

return total;

}

}

};

});

Directives are created by calling the directive method on an AngularJS module and passing in the name of the directive (cartSummary in this case) and a factory function that returns a directive definition object. The definition object defines properties that tell AngularJS what your directive does and how it does it. I have specified three properties when defining the cartSummary directive, and I have described them briefly in Table 7-2. (I describe and demonstrate the complete set of properties in Chapters 16 and 17.)

Tip Although my directive is rather basic, it isn’t the simplest approach you can use to create a directive. In Chapter 15, I show you how to create directives that use jqLite, the AngularJS version of jQuery to manipulate existing content. The kind of directive that I have created here, which specifies a template and a controller and restricts how it can be applied, is covered in Chapter 16 and Chapter 17.

In short, my directive definition defines a controller, tells AngularJS to use the components/cart/cartSummary. html view, and restricts the directive so that it can be applied only as an element. Notice that the controller in Listing 7-6 declares a dependency on the cart service, which is defined in the same module. This allows me to define the total and itemCount behaviors that consume the methods provided by the service to operate on the cart contents.

The behaviors defined by the controller are available to the partial view, which is shown in Listing 7-7.

Listing 7-7. The Contents of the cartSummary.html File

<style>

.navbar-right { float: right !important; margin-right: 5px;}

.navbar-text { margin-right: 10px; }

</style>

<div class=”navbar-right”>

<div class=”navbar-text”>

<b>Your cart:</b>

{{itemCount()}} item(s),

{{total() | currency}}

</div>

<a class=”btn btn-default navbar-btn”>Checkout</a>

</div>

Tip This partial view contains a style element to redefine some of the Bootstrap CSS for the navigation bar that runs across the top of the SportsStore layout. I don’t usually like embedding style elements in partial views, but I do so when the changes affect only that view and there is a small amount of CSS. In all other situations, I would define a separate CSS file and import it into the application’s main HTML file.

The partial view uses the controller behaviors to display the number of items and the total value of those items. There is also an a element that is labeled Checkout; clicking the button doesn’t do anything at the moment, but I’ll wire it up later in the chapter.

2.1. Applying the Cart Widget

Applying the cart widget to the application requires three steps: adding a script element to import the contents of the JavaScript file, adding a dependency for the cart module, and adding the directive element to the markup. Listing 7-8 shows all three changes applied to the app.html file.

Listing 7-8. Adding the Cart Widget to the app.html File

<!DOCTYPE html>

<html ng-app=”sportsStore”>

<head>

<title>SportsStore</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“sportsStore”, [“customFilters”, “cart”]);

</script>

<script src=”controllers/sportsStore.js”></script>

<script src=”filters/customFilters.js”></script>

<script src=”controllers/productListControllers.js”></script>

<script src=”components/cart/cart.js”></script>

</head>

<body ng-controller=”sportsStoreCtrl”>

<div class=”navbar navbar-inverse”>

<a class=”navbar-brand” href=”#”>SPORTS STORE</a>

<cart-summary />

</div>

<div class=”alert alert-danger” ng-show=”data.error”>

Error ({{data.error.status}}). The product data was not loaded.

<a href=”/app.html” class=”alert-link”>Click here to try again</a>

</div>

<ng-include src=”‘views/productList.html'”></ng-include>

</body>

</html>

Notice that although I used the name cartSummary when I defined the directive in Listing 7-8, the element I added to the app.html file is cart-summary. AngularJS normalizes component names to map between these formats, as I explain in Chapter 15. You can see the effect of the cart summary widget in Figure 7-4. The widget doesn’t do much at the moment, but I’ll start adding other features that will drive its behavior in the following sections.

3. Adding Product Selection Buttons

As with all AngularJS development, there is some up-front effort to develop the foundations and then other features start to snap into place—something that holds true for the cart as much as another part of the application. My next step is to add buttons to the product details so that the user can add products to the cart. First, I need to add a behavior to the controller for the product list view to operate on the cart. Listing 7-9 shows the changes I have made to the controllers/productListController.js file.

Listing 7-9. Adding Support for the Cart to the productListControllers.js File

angular.module(“sportsStore”)

.constant(“productListActiveClass”, “btn-primary”)

.constant(“productListPageCount”, 3)

.controller(“productListCtrl”, function ($scope, $filter,

productListActiveClass, productListPageCount, cart) {

var selectedCategory = null;

$scope.selectedPage = 1;

$scope.pageSize = productListPageCount;

$scope.selectCategory = function (newCategory) {

selectedCategory = newCategory;

$scope.selectedPage = 1;

}

$scope.selectPage = function (newPage) {

$scope.selectedPage = newPage;

}

$scope.categoryFilterFn = function (product) {

return selectedCategory == null ||

product.category == selectedCategory;

}

$scope.getCategoryClass = function (category) {

return selectedCategory == category ? productListActiveClass : ‘”‘;

}

$scope.getPageClass = function (page) {

return $scope.selectedPage == page ? productListActiveClass : ‘”‘;

}

$scope.addProductToCart = function (product) {

cart.addProduct(product.id, product.name, product.price);

}

};

I have declared a dependency on the cart service and defined a behavior called addProductToCart that takes a product object and uses it to call the addProduct method on the cart service.

Tip This pattern of declaring a dependency on a service and then selectively exposing its functionality through the scope is one you will encounter a lot in AngularJS development. Views can access only the data and behaviors that are available through the scope—although, as I demonstrated in Chapter 6 (and explain in depth in Chapter 13), scopes can inherit from one another when controllers are nested or (as I explain in Chapter 17) when directives are defined.

I can then add button elements to the partial view that displays the product details and invokes the addProductToCart behavior, as shown in Listing 7-10.

Listing 7-10. Adding Buttons to the productList.html File

<div class=”panel panel-default row” ng-controller=”productListCtrl”

ng-hide=”data.error”>

<div class=”col-xs-3″>

<a ng-click=”selectCategory()”

class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”

ng-class=”getCategoryClass(item)”>

{{item}}

</a>

</div>

<div class=”col-xs-8″>

<div class=”well”

ng-repeat=

“item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize”>

<h3>

<strong>{{item.name}}</strong>

<span class=”pull-right label label-primary”>

{{item.price | currency}}

</span>

</h3>

<button ng-click=”addProductToCart(item)”

class=”btn btn-success pull-right”>

Add to cart

</button>

<span class=”lead”>{{item.description}}</span>

</div>

<div class=”pull-right btn-group”>

<a ng-repeat=

“page in data.products | filter:categoryFilterFn | pageCount:pageSize”

ng-click=”selectPage($index + 1)” class=”btn btn-default”

ng-class=”getPageClass($index + 1)”>

  {{$index + 1}}

</a>

</div>

</div>

</div>

Tip Bootstrap lets me style a and button elements so they have the same appearance; as a consequence, I tend to use them interchangeably. That said, a elements are more useful when using URL routing, which I describe later in this chapter.

You can see the buttons and the effect they have in Figure 7-5. Clicking one of the Add to cart buttons invokes the controller behavior, which invokes the service methods, which then causes the cart summary widget to update.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *