Affiliate Marketing Shopping Cart in AngularJS Responsive with Video - CodeProject

:

Click Here to See A Working Demo of this AngularJS Shopping Cart

Click Here to Download Angular 2 Version of Shopping Cart

Click Here to See Different Version of Shopping Cart

Watch Video Demonstrating Shopping Cart Features

Angular Editor for Shoping Cart on CodeProject Posted Here - Click Here

What Is An Affiliate Marketing Cart?

Many people would like to sell products online but they don't have anny prodcts to sell. And, many people would like to have other people sell the products they have but don't know how to find those people.  This Affilaite Marketing Shopping Cart does both.  You can give this cart to distributors filled with your products to put on their websites and it will pass the orders to you from any merchant account provider like PayApal with the Distributor ID of the distributor that the order came from so you can pay them commissions.

In addition, you can also give distributors a link with their Distributor ID and those orders with the passed in Distributor ID will pass into PayPal or any designated merchant provider.

With this Affilaite Marketing Shopping Cart you can load up load up the products you wnat your distributors to sell and give your distributors a zipped up copy of your cart OR you can just give them links to your products and the cart will track the Distributor IDs and pass those Distributor IDs into the merchant payment sytem you use.

I have also made a number of major chnages to sample shopping cart.  I merged my Shopping Cart Administrator functions into the shopping cart itself so that you can now edit each item in the cart dynamically. I added customizable shipping and handling and a flag to turn sales on and off dynamically. And I updated the Distributor ID functionality so that it is easier to pay your distributors commissons. Extended our focus on palying video commercials from servers all over the world. Added the ability to connect to JSON File, Local and Remote Databases, and a Hover library to this project and a menu tab called "Effects" that allows users to add dozens of different hover animations to the pills and/or any objects in any views.

The Shopping Cart in this project includes the ability to play videos from servers all over the world. The reason for this is that pictures in shopping carts in today's world are not feective in delivery the benefits of why somebody should buy a product. A video is a TV commercial that studies have shown is thousands of times more effective in generating sales. For example, teh YOUKU server in China is now the largest television network in teh world and you can post your videos (TV commercials) for free on the YOUKU server and play them in this Shopping Cart in stead of a boring picture of a product.

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. 

To start with I wanted to include a Pinterest Style Layout so I decided to use a common one that I have seen used often in shopping carts, namely, Codrops famous ViewModeSwitch, that you can find at: https://github.com/codrops/ViewModeSwitch.  ViewModeSwitch is a CSS Pinterest Style Layout that is used n many commercial shopping and it works well with AngularJS with minimal changes.


Shopping Cart Features

Here are some of the practical features I included:

  • Added the Editor and Admistrator functions directly into the shopping cart.
  • Ability to get paid commissions and to pay commissions.  There is Distributor ID in the Config file
    that allows you to receive commssions and another Distributor ID that can be passed in as a URL
    parameter and BOTH Distributor IDs are passed into PayPal and other providers for accounting.
  • Added easily customized shipping and handling.
  • Expanded PayPal features including addition of Notify Url page and Cancel Url page.
  • Uses newer version 1.4.8 of AngularJS.
  • Must Include Bootstrap 3 without adding ui.bootstrap
  • Must be responsive so it will display and scroll perfectly on any mobile device.
    See example of mobile app using this cart at: www.SerGioApps.com
  • Must have a cool-looking, responsive Bootstrap Menu. 
  • Allow Multiple Stores in our cart.
  • Must read the products and their descriptions from an external JSON text file.
  • Must be able to play a Video (TV Commercial) about a product in addition to a picture of the product from hundreds of tube servers around the world. Must play videos posted on the YOUKU server in China which is now the largest television network in the world.
  • 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 Dialog Service WITHOUT using ui.bootstrap because we don't want the headaches of trying to keep up with their changes.
  • 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.
  • Must be able to display products in a Pinterest Style Layout or a Listview Layout
  • Must follow MVC architecture.
  • Must include Pagination to control number of products displayed per page
  • Must include Filter & Sort Options
  • Must be able to create Distributor Links for non-Angular sites for Distributor Commissions
  • Ability to add or remove a AngularJS Super Slick Carousel in the cart. Source code at:
    http://www.codeproject.com/Articles/895739/Super-Slick-Carousel-Using-AngularJS-Directive

Aministrator & Editor Now Built Into Cart

I added my AngularJS Shopping Cart Editor as shown below directly into the shopping cart.  The idea is to use the Editor in Development and to REMOVE it when you put the Shopping Cart into Production. A detailed explanation of the Editor can be found in my seprate Article here on CodeProject. Now you can easily call up the editor to edit in real time any of the items in the shopping cart.

Hover Effects

I decided to add a collection of hover animations and I wrote an animation editor that is in the menu to select and apply different hover animations to different objects in the cart. I loooked at a number of hover libraries and picked one called Hover that I really liked by Ian Lunn which you can explore on his GitHub at: https://github.com/IanLunn   

To apply an effect you simply select the Effects tab in the menu and then selct one of the green radio buttons, namely: storeimg, store pill, carousel img, or carosel pill. Note that the carousel options are only avaiable if you have added the code from my article Super Slick Carousel Using AngularJS Directive.  
After selecting the object you want to apply the hover effect to simply click on the effect you want in the list belwo. You can easily define new objects to apply these effects to in the views.

The hover effects from the Hover library are applied as follows:

// In the config.js file:
    'CF_AN_STORE_IMG_VIDEO': 'hvr-pulse-grow',
    'CF_AN_STORE_PILL': 'hvr-wobble-to-top-right',

// in the storeApp.controller('storeController', function (
$scope, $filter, $routeParams, $location, DataService, $sce, CONFIG/strong>) {
    $scope.AN_STORE_IMG_VIDEO = CONFIG.CF_AN_STORE_IMG_VIDEO;
    $scope.AN_STORE_PILL = CONFIG.CF_AN_STORE_PILL;

// Hover effects for pill & img/video classes are applied on startup.
<li ng-repeat="product in pagedItems[currentPage] | 
filter:query | orderBy:'sortorder'" class="store_pill ng-scope {{AN_STORE_PILL}}">
<div class="store_img_video {{AN_STORE_IMG_VIDEO}}">

// And animation hover effects are applied from 'Effects" Menu.
$scope.changeAnimation = function (effect_name) {
    var e = '';
    if ($scope.myModel === 'carousel_img_video') {
      e = '.carousel_img_video';
    }
    else if ($scope.myModel === 'carousel_pill') {
        e = '.carousel_pill';
    }
    else if ($scope.myModel === 'store_img_video') {
        e = '.store_img_video';
    }
    else if ($scope.myModel === 'store_pill') {
        e = '.store_pill';
    }
    if (e.length > 0) {
        // Remove all classes that start with 'hvr-' as follows:
        $(e).removeClass(function (index, css) {
            return (css.match(/(^|\s)hvr-\S+/g) || []).join(' ');
        });
        // Now apply our selcted hover effect class as follows:
        $(e).addClass(effect_name);
    }
};

AngularJS Radio Button Group

It is interesting to note that in the dropdown menu to apply the hover effects I created a directive to handle radio buttons in the shopping cart as follows:

storeApp.directive('radioButtonGroup', function () {
    return {
        restrict: 'E',
        scope: { model: '=', options: '=', id: '=', name: '=', suffix: '=', disabled: '=', showinfo: '=' },
        controller: function ($scope) {
            $scope.activate = function (option, $event) {
                if (option.showinfo.length > 0) {
                    alert(option.showinfo);
                    return false;
                }
                $scope.model = option[$scope.id];
                // stop click event to avoid Bootstrap toggling "active" class
                if ($event.stopPropagation) {
                    $event.stopPropagation();
                }
                if ($event.preventDefault) {
                    $event.preventDefault();
                }
                $event.cancelBubble = true;
                $event.returnValue = false;               
            };

            $scope.isActive = function (option) {
                return option[$scope.id] == $scope.model;
            };

            $scope.isDisabled = function (option) {
                return option[$scope.disabled] == $scope.model;
            };

            $scope.getName = function (option) {
                return option[$scope.name];
            }
        },
        template: "<button type='button' class='btn btn-{{suffix}}' " +
            "ng-class='{active: isActive(option)}'" +
            "ng-repeat='option in options' " +
            //The ng-disabled expression is evaluated in the present scope. Hence, 
            //you should NOT USE the extra interpolation with {{..}} which will not work:
            "ng-disabled=option.disabled ng-click='activate(option, $event)'><span ng-bind-html='getName(option) | unsafe'></span>" +
            "</button>"
    };
});

This directive is applied as follows in the radio button options to apply the hover effects with Bootstrap 3 styling as shown below:

// create a radioButtonGroup for our apply effects options
$scope.optActions = [
    { id: 'apply', name: 'apply effect', disabled: false, showinfo: '' },
    { id: 'remove', name: 'remove effect', disabled: false, showinfo: '' }
];
$scope.modelAction = 'apply';
$scope.idProperty = "id";
$scope.nameProperty = "name";
$scope.bootstrapSuffix = "x-success";
$scope.disabledProperty = false;
$scope.showinfoProperty = "";

// create a radioButtonGroup for our apply effects options
//{ id: 'carousel_img_video', name: 'carousel img', disabled: false, showinfo: 'You need to download and install the AngularJS Slick Carousel to apply effects to the Carousel!' },
//{ id: 'carousel_pill', name: 'carousel pill', disabled: false, showinfo: 'You need to download and install the AngularJS Slick Carousel to apply effects to the Carousel!' }
$scope.myOptions = [
    { id: 'store_img_video', name: 'store img', disabled: false, showinfo: 'You need to download and install the AngularJS Shopping Cart to apply effects to the shopping cart!' },
    { id: 'store_pill', name: 'store pill', disabled: false, showinfo: 'You need to download and install the AngularJS Shopping Cart to apply effects to the shopping cart!' },
    { id: 'carousel_img_video', name: 'carousel img', disabled: false, showinfo: '' },
    { id: 'carousel_pill', name: 'carousel pill', disabled: false, showinfo: '' }
];
$scope.myModel = 'carousel_img_video';
$scope.idProperty = "id";
$scope.nameProperty = "name";
$scope.bootstrapSuffix = "xs-success";
$scope.disabledProperty = false;
$scope.showinfoProperty = "";

Bootstrap 3 Navbars

I used Bootstrap 3 but NOT ui.bootstrap because ui.bootstrap gives me headaches trying to keep up with their changes. Bootstrap 3 has navbars where it easy to change the look-and-feel of the navbars from in side your app using AngularJS as demonstrated below in the shopping cart. To create the gradient in these navbars I used the gradient editor at: http://www.colorzilla.com/gradient-editor/.​ 

For the Navbars I applied Color Coordination with the Navbars so that each navbar would have its own Hover Effect when hovering over the pills created by Codrops famous ViewModeSwitch,  The code that changes the background color and enlarges the image as well as dozens of other cool hover effects are applied using the Hover library.  A few other transition effects are from the project on Codrops related to ViewModeSwitch called ResponsiveIconGrid at: http://tympanus.net/Blueprints/ResponsiveIconGrid/#
In each Navbar style sheet we have that hover gradient css for the pills as follows.

/* in the style sheet for the gray navbar: navbar_gray_gradient.css */
/* See:  http://www.colorzilla.com/gradient-editor/   */
.nav-pills li:hover{
    color:#fff !important;  
    background: rgb(149,149,149); /* Old browsers */
    background: -moz-linear-gradient(top,  rgba(149,149,149,1) 0%, rgba(13,13,13,1) 46%, 
    rgba(1,1,1,1) 50%, rgba(10,10,10,1) 53%, rgba(78,78,78,1) 76%, rgba(56,56,56,1) 87%, 
    rgba(27,27,27,1) 100%); /* FF3.6+ */
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(149,149,149,1)), 
    color-stop(46%,rgba(13,13,13,1)), color-stop(50%,rgba(1,1,1,1)), color-stop(53%,rgba(10,10,10,1)), 
    color-stop(76%,rgba(78,78,78,1)), color-stop(87%,rgba(56,56,56,1)), 
    color-stop(100%,rgba(27,27,27,1))); /* Chrome,Safari4+ */
    background: -webkit-linear-gradient(top,  rgba(149,149,149,1) 0%,
    rgba(13,13,13,1) 46%,rgba(1,1,1,1) 50%,rgba(10,10,10,1) 53%,rgba(78,78,78,1) 76%,
    rgba(56,56,56,1) 87%,rgba(27,27,27,1) 100%); /* Chrome10+,Safari5.1+ */
    background: -o-linear-gradient(top,  rgba(149,149,149,1) 0%,rgba(13,13,13,1) 46%,
    rgba(1,1,1,1) 50%,rgba(10,10,10,1) 53%,rgba(78,78,78,1) 76%,rgba(56,56,56,1) 87%,
    rgba(27,27,27,1) 100%); /* Opera 11.10+ */
    background: -ms-linear-gradient(top,  rgba(149,149,149,1) 0%,rgba(13,13,13,1) 46%,
    rgba(1,1,1,1) 50%,rgba(10,10,10,1) 53%,rgba(78,78,78,1) 76%,rgba(56,56,56,1) 87%,
    rgba(27,27,27,1) 100%); /* IE10+ */
    background: linear-gradient(to bottom,  rgba(149,149,149,1) 0%,rgba(13,13,13,1) 46%,
    rgba(1,1,1,1) 50%,rgba(10,10,10,1) 53%,rgba(78,78,78,1) 76%,rgba(56,56,56,1) 87%,
    rgba(27,27,27,1) 100%); /* W3C */
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#959595', 
    endColorstr='#1b1b1b',GradientType=0 ); /* IE6-9 */
}

Which produces the different hover effects for each navbar. However, we need to turn off many of these effects when the shopping cart is on a mobile device and the CSS in this project does that to achieve a nicer display on mobile devices. Dozens of samples of all kinds of animation effects for Codrops famous ViewModeSwitch are available on Codrops.  However remember that animation effects are distracting and take a reader's attention away from the ad copy for your products so use them sparingly.  Below you can see there are a large number of effects you can apply to any object in any view. The red button in the tab menu dropdown allows you remove any hover effect you have applied.

Bootstrap 3 Gradient Buttons

I don't like the falt look of default Bootstrap 3 buttons so I decided to give them some depth as shown below. To do this I used a really cool Bootstrap 3 Editor that creates buttons with a gradient and mouse over and mousedown eeffects with a single block of CSS code at: http://charliepark.org/bootstrap_buttons/

Favicon Directive

Another feature I added to the navbar is a directive to retrieve the favicons of the url links in the navbar as shown below. In the attached sample code I make a list of the most useful video sites similar to YouTube and retrieved their favicons using the following:

// In our Navbar Menu we have...
<li><a ng-href="http://www.software-rus.com/powerpinboard.html" target="_blank">
<img favicon url="'http://www.software-rus.com'" description="''">&nbsp;Source Code </a></li>
...

<script type="text/javascript">
var favApp = angular.module("favicon", []);
favApp.filter("favicon", function () {
    var provider = "https://www.google.com/s2/favicons?domain=%s";

    return function (url) {
        return provider.replace(/%s/g, url);
    }
})
.directive("favicon", ["faviconFilter", function (faviconFilter) {
    return {
        restrict: "EA",
        replace: true,
        template: '<img alt="{{description}}" />',
        scope: {
            url: "=",
            description: "="
        },
        link: function ($scope, element, attrs) {
            $scope.$watch("url", function (value) {
                $scope.faviconUrl = faviconFilter(value);
            });
        }
    }
} ]);

</script>

Dialog Service

I added a dialog service, see 'storeMessages', because I  didn't want to pollute the DOM with modal content. As a service we defer it until the point the service is called.passing the data into the modal. Whereas with a directive we would need custom attributes (that would differ from modal to modal) and that means the data would have to be put on the scope before it could be passed in, which is not always convenient. You should customize this dialog service to whatever look and feel and functionality you want in your own cart. You should customize this rudimentary dialog service I added to suite your own needs in your shopping cart. To use this dialog service simply add it as follows:

var storeApp = angular.module('AngularStore', ['ngRoute', 'storeApp.config', 
'favicon', 'igTruncate', 'storeMessages.services']).  etc....

// USE THIS TO POPUP MODAL DIALOG: 
<a href="#" ng-click="launchSimpleModal(product)">
    <img ng-src="ac_products/{{product.imagename}}" 
     alt="{{product.productname}}" class="imgcart" /></a>

JSON Editor

I will be adding an editor I  wrote in AngularJS to create and edit the JSON products.txt file where the store's products are stored. You can always find the latest code for all my projects on my website at:
www.software-rus.com.  
My goal is an AngularJS app that looks and behaves nicely on any mobiel device or laptop as show below.


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.

var storeApp = angular.module('AngularStore', ['ngRoute', 'storeApp.config', 'favicon', 'igTruncate']).
config(['$routeProvider', function ($routeProvider) {
    $routeProvider.
        when('/store', {
            templateUrl: 'ac_partials/store.htm',
            controller: 'storeController'
        }).
        when('/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 can just drop the cart into an existing website on a server at the root level and our folders will not conflict with existing folders or files.

I added a separate config.js to make it easy for users to easily setup the features for multiple stores. See below:

//Setup each store below:
var config_module = angular.module('storeApp.config', [])
.constant('CONFIG', {
    'CF_STORE_ID': 'SerGioApps',
    'CF_STORE_PAGE': 'storefront.html',
    'CF_STORE_BG_IMAGE': 'ac_img/bg1.jpg',
    'CF_DISTRIBUTOR_ID': 'WS1732',
    'CF_PAYMENT_PAYPAL_BUYNOW': 'paypaluser@youremail.com',
    'CF_PAYMENT_GOOGLE_WALLET_ID': 'GooGle_Wallet_ID',
    'CF_PAYMENT_STRIPE': 'pk_test_stripe',
    'CF_PRODUCTS_FILE': 'ac_products/products.txt',
    'CF_PRODUCTS_FOLDER': 'ac_products',
    'CF_NAVBAR_THEME': 'navbar_gray_gradient',
    'CF_NAVBAR_LOGO_TEXT': 'SerGioApps',
    'CF_NAVBAR_LOGO_LINK': 'http://www.Software-rus.com/',
    'CF_INSIDE_HEADER_SHOW': true,
    'CF_INSIDE_HEADER_LINK': 'http://www.Software-rus.com/',
    'CF_INSIDE_HEADER_IMAGE': 'ac_img/logo.png',
    'CF_INSIDE_HEADER_TITLE': 'Responsive Angular Cart',
    'CF_CAROUSEL_SHOW': false,
    'CF_CAROUSEL_AUTO_PLAY': true,
    'CF_AN_CAROUSEL_IMG_VIDEO': 'hvr-pulse-grow',
    'CF_AN_CAROUSEL_PILL': 'hvr-wobble-to-top-right',
    'CF_AN_STORE_IMG_VIDEO': 'hvr-pulse-grow',
    'CF_AN_STORE_PILL': 'hvr-wobble-to-top-right',
    'CF_SYSTEM_NAME': 'My Angular JS project',
    'CF_SYSTEM_LANGUAGE': '',
    'CF_BASE_URL': '',
    'CF_API_URL': '',
    'CF_GOOGLE_ANALYTICS_ID': '',
    'CF_DB': null,
    'CF_DATABASENAME': 'serviceapp',
    'CF_TABLE1': 'serviceorders',
    'CF_TABLE2': 'photos',
    'CF_KEYPATH1': 'primarykey',
    'CF_KEYPATH2': 'data',
    'CF_INDEX1': 'needsynchronization',
    'CF_INDEX2': 'ordernumber',
    'CF_INDEX3': 'timestamp',
    'CF_DB_VERSION': 1,
    'CF_GLOBALCOUNTER': 0,
    'CF_LOADED': false,
    'CF_ADDRESS': [' 3156 Dusty Highway', ' Teaneck New Jersey 07009-6370 US']
});

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).
// Notice that we can use a JSON file, or a local or remote database for our products
storeApp.factory('DataService', function ($http, $q) {
    var _DATA_SOURCE = CONFIG.CF_DATA_SOURCE;
    if (localStorage["data_src"]) {
        _DATA_SOURCE = localStorage["data_src"];
    } else {
        _DATA_SOURCE = "ac_products/products.txt";
        localStorage["data_src"] = _DATA_SOURCE;
    }
    if (_DATA_SOURCE == "localdb") {
        _DATA_SOURCE = "/crud.ashx?ac=getproducts&cn=local"
    }
    else if (_DATA_SOURCE == "remotedb") {
        _DATA_SOURCE = "/crud.ashx?ac=getproducts&cn=remote"
    }
    CONFIG.CF_DATA_SOURCE = _DATA_SOURCE;

    function Store() {
        var productsDeferred = $q.defer();
        this.products = productsDeferred.promise; //this.products is a promise
        // Store can be loadded from a JSON file, a local .mdf database, or a remote database.
        //'CF_DATA_SOURCE': 'ac_products/products.txt',
        //'CF_DATA_SOURCE': '/crud.ashx?ac=getproducts&cn=local',
        //'CF_DATA_SOURCE': '/crud.ashx?ac=getproducts&cn=remote',
        $http.get(CONFIG.CF_DATA_SOURCE).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 and pass in our CONFIG
    var myCart = new shoppingCart("AngularStore", CONFIG);
    
    // 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", "paypaluser@youremail.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
    };
});

The Bootstrap 3 Menu Control

I kept the menu control for our Bootstrap 3 menu very simple as you can see below. To set the active tab I used ng-controller to run a single controller outside of the ng-view as shown below.

<li ng-class="{ active: isActive('/store')}"><a ng-href="storefront.html#/store">Store </a></li>

and... 

storeApp.controller('MyMenu', function ($scope, $filter, $location, CONFIG) {

    $scope.name = 'MyMenu';
    $scope.isCollapsed = false;
    $scope.dataLoaded = false;

    /*#####################
    CONFIG
    ######################*/
    /* our global variabls */
    $scope.STORE_ID = CONFIG.STORE_ID;
    $scope.STORE_PAGE = CONFIG.STORE_PAGE;
    $scope.STORE_BG_IMAGE = CONFIG.STORE_BG_IMAGE;
    $scope.DISTRIBUTOR_ID = CONFIG.DISTRIBUTOR_ID;
    $scope.PAYMENT_PAYPAL_BUYNOW = CONFIG.PAYMENT_PAYPAL_BUYNOW;
    $scope.PAYMENT_GOOGLE_WALLET_ID = CONFIG.PAYMENT_GOOGLE_WALLET_ID;
    $scope.PAYMENT_STRIPE = CONFIG.PAYMENT_STRIPE;
    $scope.PRODUCTS_FILE = CONFIG.PRODUCTS_FILE;
    $scope.PRODUCTS_FOLDER = CONFIG.PRODUCTS_FOLDER;
    $scope.NAVBAR_THEME = CONFIG.NAVBAR_THEME;
    $scope.NAVBAR_LOGO_TEXT = CONFIG.NAVBAR_LOGO_TEXT;
    $scope.NAVBAR_LOGO_LINK = CONFIG.NAVBAR_LOGO_LINK;
    $scope.INSIDE_HEADER_SHOW = CONFIG.INSIDE_HEADER_SHOW;
    $scope.INSIDE_HEADER_LINK = CONFIG.INSIDE_HEADER_LINK;
    $scope.INSIDE_HEADER_IMAGE = CONFIG.INSIDE_HEADER_IMAGE;
    $scope.INSIDE_HEADER_TITLE = CONFIG.INSIDE_HEADER_TITLE;
    $scope.CAROUSEL_SHOW = CONFIG.CAROUSEL_SHOW;
    $scope.CAROUSEL_AUTO_PLAY = CONFIG.CAROUSEL_AUTO_PLAY;
    $scope.SYSTEM_NAME = CONFIG.SYSTEM_NAME;
    $scope.SYSTEM_LANGUAGE = CONFIG.SYSTEM_LANGUAGE;
    $scope.BASE_URL = CONFIG.BASE_URL;
    $scope.API_URL = CONFIG.API_URL;
    $scope.GOOGLE_ANALYTICS_ID = CONFIG.GOOGLE_ANALYTICS_ID;


    if ($scope.CAROUSEL_SHOW) {
        $('#storeslider_wrapper').css('display', 'block');
    }
    else {
        $('#storeslider_wrapper').css('display', 'none');
    }

    if ($scope.INSIDE_HEADER_SHOW) {
        $('.inside_header').css('display', 'block');
    }
    else {
        $('.inside_header').css('display', 'none');
    }

    if ($scope.STORE_BG_IMAGE.length > 0) {
        $("body").css('background-image', '');
        $("body").css("background", "#ffffff url(" + $scope.STORE_BG_IMAGE + ") no-repeat center center fixed");
        localStorage['bg_cart'] = $scope.STORE_BG_IMAGE;
    }

    _navbar_theme = "navbar_gray_gradient";
    if (localStorage["navbar_theme"]) {
        _navbar_theme = localStorage["navbar_theme"];
    } else {
        _navbar_theme = "navbar_gray_gradient";
        localStorage["navbar_theme"] = "navbar_gray_gradient";
    }
    var _path = "ac_css/" + _navbar_theme + ".css";
    $("#link_index").attr("href", _path);
    $scope.NAVBAR_THEME = _navbar_theme;

    $scope.changeBackgroundImage = function (event) {
        //event.stopPropagation();
        event.preventDefault();
        var x = 0
        for (x = 0; x < arBGs.length; x++) {
            if (_bgImage === arBGs[x]) { break; }
        }
        if (x + 1 < arBGs.length) {
            _bgImage = arBGs[x + 1];
        }
        else {
            x = 0;
            _bgImage = arBGs[x];
        }
        $("body").css('background-image', '');
        $("body").css("background", "#ffffff url(" + _bgImage + ") no-repeat center center fixed");
        localStorage['bg_cart'] = _bgImage;
    }

    $scope.changeNavBar = function (css_name) {
        //event.stopPropagation();
        event.preventDefault();
        var _path = "ac_css/" + css_name + ".css";
        _navbar_theme = css_name;
        localStorage["navbar_theme"] = _navbar_theme;
        $("#link_index").attr("href", _path);
        return false;
    };

    // Author: Bill SerGio - An elegant way to set the active tab is to use ng-controller 
    // to run a single controller outside of the ng-view as shown below.
    $scope.isActive = function (viewLocation) {
        return viewLocation === $location.path();
    };
});

Our Angular Views: Store, Product, and Cart

The look and feel of a shopping cart is very important so I decided to use a Pinterest Style Layout that can be switched with a ListView Layout which is commonly seen in more expensive shopping carts. I used as a starting point a non-AngularJS css layout called ViewModeSwitch that I modified for AngularJS that I found on GitHub at: https://github.com/codrops/ViewModeSwitch

When Bernardo Castilho wrote his article and cart he used a version of AngularJS earlier than AngularJS 1.2 and he didn't apply HTML formatting to any of his titles or product descriptions.  In the cart in this article I have added HTML formatting to the titles an d descriptions. HOEVER to do this in the cart in this article which uses version 1.3.15 of AngularJS (also works fine with AngularJS 1.4 beta) you now have to use $sce to run your output through a filter, which you can specify to allow the HTML.  I implemented this using $sce.trustAsHtml() by adding a filter to my code. This custom filter will make sure that our HTML in ALL our views doesn’t get filtered out by AngularJS 1.2/AngularJS 1.3 or later.  I named this filter “unsafe”. It gets passed a value, which we will return as trusted HTML output as shown here.

commentApp.filter('unsafe', function($sce) {
    return function(val) {
        return $sce.trustAsHtml(val);
    };
});
USAGE: <p ng-bind-html="product.shortdesc| unsafe"></p>

Another thing that wasn't in the cart in the original articles was video which is a nice feature to have in any shopping cart. To accomplish displaying video in AngularJS versions greater than AngularJS 1.2 I created a directive using $sce.trustAsResourceUrl as shown below..

 storeApp.directive('myYoutube', function ($sce) {
    return {
        restrict: 'EA',
        scope: { code: '=' },
        replace: true,
        template: '<div class="video-container" style="height:200px !important;">
                   <iframe style="overflow:hidden;max-height:200px !important;
                   max-width:300px !important" src="{{url}}" frameborder="0" 
                   allowfullscreen></iframe></div>',
        link: function (scope) {
            console.log('here');
            scope.$watch('code', function (newVal) {
                if (newVal) {
                    scope.url = $sce.trustAsResourceUrl("http://www.youtube.com/embed/" + newVal);
                }
            });
        }
    };
});
<div my-youtube code="product.videoid"></div>

The next thing that I think every shopping cart needs is the ability to give out Distributor Links to your distributors so that a distributor can place a link on their non-Angular website or in a PhoneGap or Cordova mobile app that will pass the Distributor ID Code into your cart AND add that product into the cart on the checkout page if and only if it is NOT already in our cart. I added this feature by using url parameters like the link shown below.: 

<a href="http://www.your_site.com/storefront.html?
      distid=SOME_DIST_ID
      &sku=SOME_PRODUCT_SKU#/cart" 

      target="_blank" class="btn btn-sm btn-default">Purchase Cart Now</a>

This link will paas in the distid and sku as url parameters.

        $scope.getUrlVar = function (key) {
            var result = new RegExp(key + "=([^&]*)", "i").exec(window.location.search);
            return result && unescape(result[1]) || "";
        }
        var _sku = $scope.getUrlVar('sku');
        if (_sku.length > 0) {
            for (var i = 0, len = $scope.products.length; i < len; i++) {
                var prod = $scope.products[i];
                if (prod.sku === _sku) {
                    DataService.cart.addItemUrl(prod.sku, prod.productname, prod.unitprice, +1);
                }
            }
        }
        var _distid = $scope.getUrlVar('distid');
        if (_distid.length > 0) {
            // Do something with _distid so you can pay the commissions you owe!
        }

And in our shoppingCart.js code we have:

[__strong__]// adds an item to the cart from non-angular page using url parameters
shoppingCart.prototype.addItemUrl = function (sku, productname, unitprice, quantity) {
    var quantity = this.toNumber(quantity);
    var unitprice = this.toNumber(unitprice);
    if (unitprice > 0) {
        // update quantity for existing item
        var found = false;
        for (var i = 0; i < this.items.length && !found; i++) {
            var item = this.items[i];
            if (item.sku == sku) {
                found = true;
                //item.quantity = this.toNumber(item.quantity + quantity);
                //if (item.quantity <= 0) {
                //    this.items.splice(i, 1);
                //}
            }
        }
        // WE ONLY ADD ITEM TO CART IF IT IS NOT ALREADY IN THE CART !!!
        // new item, add now
        if (!found) {
            var item = new cartItem(sku, productname, unitprice, quantity);
            this.items.push(item);
        }
        // save changes
        this.saveItems();
    }
    else {alert("It's FREE, no need to add to cart!");}
}

We will create our Category Filter Buttons dynamically as follows:

[__strong__]// adds an item to the cart from non-angular page using url parameters
// AUTHOR: Bill SerGio
// Create a list of categoryname button to set categoryname filter
storeApp.filter('unique', function () {
    return function (collection, keyname) {
        var output = [],
            keys = [];

        angular.forEach(collection, function (item) {
            var key = item[keyname];
            if (keys.indexOf(key) === -1) {
                keys.push(key);
                output.push(item);
            }
        });
        return output;
    };
});

We need to force the page to scroll to the top whenever we load a new view as follows: 

// AUTHOR: Bil SerGio
// Angular doesn't automatically scroll to the top when loading a new view, 
// it just keeps the current scroll position.
// The script below checks every 200ms if the new DOM is fully loaded and then 
// scrolls to the top and stops checking. I tried without this 200ms loop and it 
// sometimes failed to scroll because the page was just not completely displayed.
storeApp.run(function ($rootScope, $window) {
    $rootScope.$on('$routeChangeSuccess', function () {
        var interval = setInterval(function () {
            if (document.readyState == 'complete') {
                $window.scrollTo(0, 0);
                clearInterval(interval);
            }
        }, 200);
    });
});

Our responsive AngularJS 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. 

You can easily add an AngularJS Super Slick Carousel by just dropping in the code. If you want to add a carousel go to: http://www.codeproject.com/Articles/895739/Super-Slick-Carousel-Using-AngularJS-Directive

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. I used the code referenced by Bernardo Castilho in his article writes: "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:https://github.com/kenyee/angularjs-cart
Hoever, all of the code for gateways in this project like PayPal, Google Wallet, and Stripe MUST be customized to fit each user's particular needs and data.. 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:

  1. 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
  2. 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
  3. 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
// - DataService: called to retrieve products from JSON file
//
storeApp.controller('storeController', function ($scope, $filter, 
$routeParams, $location, DataService, $sce, CONFIG) {
    $scope.dataLoaded = false;
    $scope.DATA_SOURCE = CONFIG.CF_DATA_SOURCE;
    if (localStorage["data_src"]) {
        $scope.DATA_SOURCE = localStorage["data_src"];
    } else {
        $scope.DATA_SOURCE = "ac_products/products.txt";
        localStorage["data_src"] = $scope.DATA_SOURCE;
    }
    CONFIG.CF_DATA_SOURCE = $scope.DATA_SOURCE;

    /*#####################
    CONFIG
    ######################*/
    /* our global variabls */
    $scope.STORE_ID = CONFIG.CF_STORE_ID;
    $scope.STORE_PAGE = CONFIG.CF_STORE_PAGE;
    $scope.STORE_BG_IMAGE = CONFIG.CF_STORE_BG_IMAGE;
    $scope.DISTRIBUTOR_ID = CONFIG.CF_DISTRIBUTOR_ID;
    $scope.PAYMENT_PAYPAL_BUYNOW = CONFIG.CF_PAYMENT_PAYPAL_BUYNOW;
    $scope.PAYMENT_GOOGLE_WALLET_ID = CONFIG.CF_PAYMENT_GOOGLE_WALLET_ID;
    $scope.PAYMENT_STRIPE = CONFIG.CF_PAYMENT_STRIPE;
    $scope.PRODUCTS_FILE = CONFIG.CF_PRODUCTS_FILE;
    $scope.PRODUCTS_FOLDER = CONFIG.CF_PRODUCTS_FOLDER;
    $scope.NAVBAR_THEME = CONFIG.CF_NAVBAR_THEME;
    $scope.NAVBAR_LOGO_TEXT = CONFIG.CF_NAVBAR_LOGO_TEXT;
    $scope.NAVBAR_LOGO_LINK = CONFIG.CF_NAVBAR_LOGO_LINK;
    $scope.INSIDE_HEADER_SHOW = CONFIG.CF_INSIDE_HEADER_SHOW;
    $scope.INSIDE_HEADER_LINK = CONFIG.CF_INSIDE_HEADER_LINK;
    $scope.INSIDE_HEADER_IMAGE = CONFIG.CF_INSIDE_HEADER_IMAGE;
    $scope.INSIDE_HEADER_TITLE = CONFIG.CF_INSIDE_HEADER_TITLE;
    $scope.CAROUSEL_SHOW = CONFIG.CF_CAROUSEL_SHOW;
    $scope.CAROUSEL_AUTO_PLAY = CONFIG.CF_CAROUSEL_AUTO_PLAY;
    $scope.AN_CAROUSEL_IMG_VIDEO = CONFIG.CF_AN_CAROUSEL_IMG_VIDEO;
    $scope.AN_CAROUSEL_PILL = CONFIG.CF_AN_CAROUSEL_PILL;
    $scope.AN_STORE_IMG_VIDEO = CONFIG.CF_AN_STORE_IMG_VIDEO;
    $scope.AN_STORE_PILL = CONFIG.CF_AN_STORE_PILL;
    $scope.SYSTEM_NAME = CONFIG.CF_SYSTEM_NAME;
    $scope.SYSTEM_LANGUAGE = CONFIG.CF_SYSTEM_LANGUAGE;
    $scope.BASE_URL = CONFIG.CF_BASE_URL;
    $scope.API_URL = CONFIG.CF_API_URL;
    $scope.GOOGLE_ANALYTICS_ID = CONFIG.CF_GOOGLE_ANALYTICS_ID;
    /* for future versions */
    //$scope.DB = CONFIG.DB;
    //$scope.DATABASENAME = CONFIG.DATABASENAME;
    //$scope.TABLE1 = CONFIG.TABLE1;
    //$scope.TABLE2 = CONFIG.TABLE2;
    //$scope.KEYPATH1 = CONFIG.KEYPATH1;
    //$scope.KEYPATH2 = CONFIG.KEYPATH2;
    //$scope.INDEX1 = CONFIG.INDEX1;
    //$scope.INDEX2 = CONFIG.INDEX2;
    //$scope.INDEX3 = CONFIG.INDEX3;
    //$scope.DB_VERSION = CONFIG.DB_VERSION;
    //$scope.GLOBALCOUNTER = CONFIG.GLOBALCOUNTER;
    //$scope.LOADED = CONFIG.LOADED;
    //$scope.SERVICEORDERS = CONFIG.SERVICEORDERS;

    $scope.filteredItems = [];
    $scope.groupedItems = [];
    $scope.pagedItems = [];

    $scope.currentPage = 1;
    $scope.pageSize = 9;

    $scope.products = [];
    $scope.slides = [];

    $scope.isActive = false;

    /* NOTE: To start with I wanted to include a Pinterest Style Layout so I decided to use 
       a common one that I have seen used often, namely, Codrops famous ViewModeSwitch, that 
       you can find at: https://github.com/codrops/ViewModeSwitch.  
       ViewModeSwitch is a CSS solution which can be incorporated "AS IS" into an AnglarJS project 
       that dozens of people have copied for use in both AngularJS and non-AngularJS shopping carts. 
       It provides the CSS Styling and animation used in this project.
    */
    $scope.sections = [{ name: 'list', class: 'cbp-vm-icon cbp-vm-list' }];

    $scope.updateDisplay = function (section) {
        $scope.selected = section;
        $scope.isActive = !$scope.isActive;
        if (section.class.toString() === 'cbp-vm-icon cbp-vm-grid') {
            $scope.sections = [{ name: 'list', class: 'cbp-vm-icon cbp-vm-list' }];
        }
        else {
            $scope.sections = [{ name: 'grid', class: 'cbp-vm-icon cbp-vm-grid' }];
        }
    }
    $scope.isSelected = function (section) {
        return $scope.selected === section;
    }
    $scope.fToggleOverlay = function () {
        $scope.overlayFlag = !$scope.overlayFlag; // toggle state of overlay flag.
    };
    // get store and cart from service
    $scope.store = DataService.store;
    $scope.cart = DataService.cart;

    // use routing to pick the selected product
    if ($routeParams.productSku != null) {
        $scope.product = $scope.store.getProduct($routeParams.productSku);
    }

    DataService.store.getProducts().then(function (data) {
        // Build array for products
        $scope.products = data;

        // Build slides[] array for super slick carousel(not in this sample project)
        for (var i = 0, len = $scope.products.length; i < len;  i++ ) {
            var prod = $scope.products[i];
            if (prod.storeid == "7cc6cb94-0938-4675-b84e-6b97ada53978") {
                if (prod.imagename.length < 1) {
                    prod.imagename = "nopic.png";
                }
                if (prod.carousel) {
                    $scope.slides.push(prod);
                }
            }
        }
        $scope.dataLoaded = true;
        if ($routeParams.productSku != null) {
            var _sku = $routeParams.productSku.toString();
            //if (_sku.length > 0) {
                for (var i = 0, len = $scope.products.length; i < len; i++) {
                    var prod = $scope.products[i];
                    if (prod.sku === _sku) {
                        $scope.product = prod;
                    }
                }
            //}
        }
        
        //////////////////////////////////////////////////////////////////////////////
        // BILL SERGIO - Code below alows Links on NON-AngularJS websitesis to "add to cart" 
        // The standard "getUrlVars" has a LOT of bugs but the code below works perfectly!
        // Given a query string "?to=email&why=because&first=John&Last=smith"
        // getUrlVar("to")  will return "email"
        // getUrlVar("last") will return "smith"
        // To convert it to a jQuery plug-in, you could try something like this:
        //(function ($) {
        //    $.getUrlVar = function (key) {
        //        var result = new RegExp(key + "=([^&]*)", "i").exec(window.location.search);
        //        return result && unescape(result[1]) || "";
        //    };
        //})(jQuery);
        $scope.getUrlVar = function (key) {
            var result = new RegExp(key + "=([^&]*)", "i").exec(window.location.search);
            return result && unescape(result[1]) || "";
        }
        var _sku = $scope.getUrlVar('sku');
        if (_sku.length > 0) {
            for (var i = 0, len = $scope.products.length; i < len; i++) {
                var prod = $scope.products[i];
                if (prod.sku === _sku) {
                    DataService.cart.addItemUrl(prod.sku, prod.productname, prod.unitprice, +1);
                }
            }
        }
        //////////////////////////////////////////////////////////////////////////////

        $scope.pageCount = function () {
            return Math.ceil($scope.products.length / $scope.pageSize);
        };
        $scope.nextPage = function () {
            if($scope.currentPage >= Math.ceil($scope.products.length / $scope.pageSize) - 1) {
                return true;
            }
            else {
                return false;
            }
        };
        var searchMatch = function (haystack, needle) {
            if (!needle) {
                return true;
            }
            return haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
        };
        $scope.filterCategory = function (categoryname) {
            //$('#searchfield').val('');
            $scope.filteredItems = $filter('filter')($scope.products, function (product) {
                for (var attr in product) {
                    if (searchMatch(product[categoryname], $scope.query))
                        return true;
                }
                return false;
            });
            $scope.currentPage = 0;
            $scope.groupedPages();
        };
        $scope.filterCategory = function (column, categoryname) {
            //$('#searchfield').val('');
            $scope.filteredItems = $filter('filter')($scope.products, function (product) {
                for (var attr in product) {
                    if (searchMatch(product[column], categoryname))
                        return true;
                }
                return false;
            });
            $scope.currentPage = 0;
            $scope.groupedPages();
        };
        $scope.groupedPages = function () {
            $scope.pagedItems = [];
            for (var i = 0; i < $scope.filteredItems.length; i++) {
                if (i % $scope.pageSize === 0) {
                    $scope.pagedItems[Math.floor(i / $scope.pageSize)] = [$scope.filteredItems[i]];
                } else {
                    $scope.pagedItems[Math.floor(i / $scope.pageSize)].push($scope.filteredItems[i]);
                }
            }
        }; 
        // functions have been describe process the data for display
        $scope.filterCategory();
        $scope.search();
 }); /* END - DataService */
});

The JSON 'products.js' 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&#174;",
        "storeid": "7cc6cb94-0938-4675-b84e-6b97ada53978",
        "categoryname": "software",
        "header": "In the Wild Workout&#174; Mobile App You Watch Wild Animals execising!",
        "shortdesc": "In the Wild Workout&#174; Mobile App you watch Wild Animals execising! And see the amazing results people are getting doing this Wild Workout.",
        "description": "Exercises based on the principles of how the wild animals stay in shape. In the Wild Workout&#174; 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": "http://www.software-rus.com/simulator2.html?app=wildworkout",
        "linktext": "try it",
        "imageurl": "",
        "imagename": "ww.gif",
        "carousel": true,
        "carousel_caption": "Watch Wild Animals Exercise!",
        "tube": "youtube",
        "videoid": "YyZNIarRYSc",
        "showvideo": true,
        "unitprice": 0,
        "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("notify_url", "http://www.YOUR_DOMAIN.com/ac_notify.html");
    form.attr("cancel_return", "http://www.YOUR_DOMAIN.com/ac_cancel.html");

    ///////////////////////////////////////////////////////////
    //PayPal's custom html variable returned to ac_notify.html
    //NOTE: _did  is the default Distributor ID in the Config file
    //_did2 is the Ditributor ID passed in as a URL parameter.
    //These Distributor IDs allow you to get paid commissions 
    //and to pay commissions bi-directionally 
    var _did = localStorage["ac_distributorid"];
    var _did2 = localStorage["ac_distributorid2"];

    form.attr("custom", _did + "|" + _did2);
    form.attr("item_number", "ac_item_number");
    form.attr("item_number_x", "ac_item_number_x");
    form.attr("invoice", "ac_invoice");
    ///////////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////////////
    //form.attr("cmd", "cmd");
    //form.attr("business", "business");
    //form.attr("lc", "lc");
    //form.attr("item_name", "item_name");
    //form.attr("item_number", "item_number");
    //form.attr("amount", "amount");
    //form.attr("currency_code", "currency_code");
    //form.attr("button_subtype", "button_subtype");
    //form.attr("no_note", "no_note");
    //form.attr("cn", "cn");
    //form.attr("no_shipping", "no_shipping");
    //form.attr("rm", "rm");
    //form.attr("return", "return_URL");
    //form.attr("cancel_return" + cancel_return_URL");
    //form.attr("shipping", "shipping");
    //form.attr("bn", "bn");
    //form.attr("address_override", "address_override");
    //form.attr("notify_url", "notify_url");
    ////////////////////////////////////////////////////////////////

    //form.attr("image_url" + image_url");
    //form.attr("cpp_cart_border_color", "cpp_cart_border_color");
    //form.attr("cpp_header_image" + cpp_header_image");
    //form.attr("cpp_headerback_color", "cpp_headerback_color");
    //form.attr("cpp_logo_image", "cpp_logo_image");

    //form.attr("invoice", "invoice");
    //form.attr("custom", "custom");
    //form.attr("baseamt", "baseamt");
    //form.attr("quantity", "quantity");
    ////form.attr("undefined_quantity", "undefined_quantity");  
    //form.attr("tax_rate=0"");
    //form.attr("handling", "handling");
    //form.attr("basedes", "basedes");
    //form.attr("page_style", "page_style");
    //form.attr("cbt", "cbt");

    ////////////////////////////////////////////////////////////////
    //form.attr("address1", "ship_address1"); //100
    //form.attr("address2", "ship_address2"); //100
    //form.attr("city", "ship_city");     //40
    //form.attr("country", "ship_country");     //2
    //form.attr("email", "ship_email");    //127
    //form.attr("first_name", "ship_first_name");    //32
    //form.attr("last_name", "ship_last_name");    //64
    //form.attr("lc", "ship_lc");    // The default is US 2
    //form.attr("charset", "ship_charset");    //see Setting the Character Set – charset.
    //form.attr("night_phone_a", "ship_night_phone_a");
    //form.attr("night_phone_b", "ship_night_phone_b");
    //form.attr("state", "ship_state");    //2
    //form.attr("zip", "ship_zip");
    ////////////////////////////////////////////////////////////////

    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.

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:

  1. Modify the addCheckoutParameters method to accept the new service name.
  2. 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.
  3. 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>
 ...

Notice the following important points:

  1. 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.
  2. 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.
  3. 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.
  4. 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 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. 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 layout 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, as is the shopping cart itself, in shoppingCart.htm.

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 “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

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/.

There are a number of additional features I plan on adding shortly like an AngularJS Slick Carousel.  And I will also be posting a version of this shopping cart written using AngularJS 2.0 in the next few weeks.