Building AngularJS UI-Grid / Bootstrap Application using Spring MVC Rest and MongoDB Backend

Building AngularJS UI-Grid / Bootstrap Application using Spring MVC Rest and MongoDB Backend

In the last tutorial, “Spring MVC RESTful Web Service Example with Spring Data for MongoDB, AngularJS, Bootstrap and Grid-UI” we covered the configuration and setup of Spring MVC for use as a RESTful API using MongoDB for our datastore. In this post, we will focus on the front-end components using AngularJS, Bootstrap, Cascading Style Sheets (CSS), JQuery, Spinner, Tree-Control and UI-Grid.

Our AngularJS Bootstrap Application

Getting Started using Spring Data

The primary goal of Spring Data is to make it easy to access both legacy relational databases in addition to new data technologies like NoSQL databases, map reduce frameworks and cloud based solutions. Spring Data for MongoDB is an umbrella project which aim to keep the consistent and familiar manner of the Spring based programming paradigm for new datastores.

In order to run this tutorial yourself, you will need the following:

Complete Project Overview

I have added the project overview to give you a full view of the structure and show you all files contained in this sample project. NOTE: You will notice that for this project I am focusing on the UI components, hence the project overview is focused on these elements.

angularjs_grid_ui_proj1

RESTful Web Service End Points

#URIMethodDescription
1/rest/booksGETReturns a list of all the books available in MongoDB repository
2/rest/categoriesGETReturns a list of all the book categories in MongoDB repository
3/rest/getbookbyisbn?isbn13={book isbn13}GETRetrieve a book using ISBN13 as the key
4/rest/findbytitle?title={regex pattern}GETFind all books by title using regular expression
5/rest/findbycategory?category={book category}&title={regex pattern}GETFind all books in an given category and use title using regular expression, if title is provided as parameter.
6/rest/book/delete?isbn13={book isbn13}DELETEDelete a book in the MongoDB based on the ISBN13
7/rest/book/update?isbn13={book isbn13}PUTUpdate a book in the MongoDB based on the ISBN13
8/rest/book/add?isbn13={book isbn13}POSTInserts the book into the MongoDB datastore based on the contents of the AngularJS form

GUI Web Page Elements

AngularJS Directives

Since this is an AngularJS SPA application, you will notice the familiar angularJS ng-app directive near the top of the page. I have gotten accustomed to putting it on the html tag, although you could put it other tags, if you wish. The ng-app directive to auto-bootstrap an AngularJS application. The ng-app directive designates the root element of the application.

The next directive that’s worth mentioning is the ng-controller directive. The function of the ng-controller directive is to attach a controller class to the view (your html page).

For more details or for a quick refresher on AngularJS, please visit my simple tutorials on AngularJS Web Development

Meta Tags

The first few meta tags are in place to prevent the page from being cached locally on the browser. These include cache-control, expires, and pragma tags.

The second meta tag that I would like to point out is the X-UA-Compatible tag. Using this tag tell Internet Explorer to display content in the highest available mode available.

Internet Explorer Edge Mode Meta Tag

<meta http-equiv=”X-UA-Compatible” content=”IE=edge” />

Edge mode tells Internet Explorer to display content in the highest mode available. With Internet Explorer 9, this is equivalent to IE9 mode. If a future release of Internet Explorer supported a higher compatibility mode, pages set to edge mode would appear in the highest mode supported by that version.

Bootstrap Features and Elements

Bootstrap Modal Windows

The Bootstrap Modal is a dialog box that is used to provide important information to the user or ask the user input in order to take some action. Modal windows are used throughout all common user interfaces to alert user or to elicit responses before proceeding.

Using bootstrap one can easily create elegant dialog boxes using the Bootstrap Modal plugin.

AngularJS Error Modal Directive

AngularJS Error Modal Directive (app.js snippet)

The code snippet will create the appropriate Bootstrap code necessary in generating the modal window. We will show the title of Modal dialog window using the variable modal.error.title. Additionally, this code will create a registered watcher using scope.$watch for the visibile attribute with a listener to be called back in the event the variable changes. This will allow us to either show or hide the modal window at any time simply by changing the value of that boolean.

Finally, using the ng-transclude directive we are able to set the insertion point for any additional code that we can use to customize the modal dialog window.

app.directive('errorModal', function () {
    return {
      template: '<div class="modal fade">' + 
          '<div class="modal-dialog">' + 
            '<div class="modal-content">' + 
              '<div class="modal-header-error">' + 
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="false">&times;</button>' +
                '<h4 class="modal-title-error">'+
                '<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>' + 
                '&nbsp;&nbsp;{{ modal.error.title }}</h4>' + 
              '</div>' + 
            '<div class="modal-body" ng-transclude></div>' + 
            '</div>' + 
          '</div>' + 
        '</div>',
      restrict: 'E',
      transclude: true,
      replace:true,
      scope:true,
      link: function postLink(scope, element, attrs) {
        scope.title = attrs.title;

        scope.$watch(attrs.visible, function(value){
          if(value == true) {
            $(element).modal('show');
            scope.tt_isOpen = false;
          } else
            $(element).modal('hide');
        });

        $(element).on('shown.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = true;
          });
        });

        $(element).on('hidden.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = false;
          });
        });
      }
    };
  });

AngularJS Error Modal Directive (index.jsp snippet)

Using the directive that was created in app.js code we are able to control the visibility of the modal window using the boolean flag called showErrorModal. We will set this flag based on some error conditions after setting the appropriate title and message fields.

  <error-modal title="Error" visible="showErrorModal">
  <div class="modal-body">
    <span class="errorText">{{modal.error.message}}</span>
  </div>
  <div class="modal-footer">
    <button class="btn btn-danger" ng-click="closeError()">OK</button>
  </div>
  </error-modal>

Error Modal Window

Application Panels

The main application user interface is arranged around three main panels (north, west and center) to give the application and nice user look and feel. We have added form style components on the north panel to facilitate searching functionality. The West panel contains a Tree Control component to allow users to easily choose categories of books and the main or center panel contains the active ui-grid component which will allow the users to interact with the actual books.

GUI Web Page — Complete Source Code (index.jsp)

<%@ page language="java" %>

<html ng-app="app">
<head>
  <meta http-equiv="cache-control" content="max-age=0" />
  <meta http-equiv="cache-control" content="no-cache" />
  <meta http-equiv="expires" content="0" />
  <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
  <meta http-equiv="pragma" content="no-cache" /> 
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  
  <!--[if lte IE 7]>
    <style type="text/css"> body { font-size: 85%; } </style>
  <![endif]-->

  <!-- 1.3.15 -->
  <script src="../../include/angular.js"></script>
  <script src="../../include/angular-touch.js"></script>
  <script src="../../include/angular-animate.js"></script>
  <script src="../../include/app.js"></script>
  <script src="../../include/jquery-1.11.3.js"></script>
  <script src="../../include/jquery.layout.js"></script>
  <script src="../../include/spin.js"></script>
  <script src="../../include/angular-spinner.js"></script>
  <script src="../../include/angular-tree-control.js"></script>
  <script src="../../include/vfs_fonts.js"></script>
  <script src="../../include/ui-grid-unstable.js"></script>
  <link rel="stylesheet" href="../../include/ui-grid-unstable.css">
  <link rel="stylesheet" href="../../include/font-awesome.min.css">
  
  <script src="../../include/ui-bootstrap-tpls-0.13.0.min.js"></script>
  <script src="../../include/bootstrap.js"></script>
  <link rel="stylesheet" href="../../include/bootstrap.css">
  
  <link rel="styleSheet" href="../../include/tree-control.css" />
  <link rel="styleSheet" href="../../include/tree-control-attribute.css" />
  <link rel="styleSheet" href="../../include/styles.css" />
</head>

<%
  // This is JSP code to define RESTful call URL to get Manager List JSON data
  // can be replaced with any other way that you're using to define RESTful URL with parameters
  String fullProtocol = request.getProtocol().toLowerCase();
  String protocol[] = fullProtocol.split("/");
  String baseUrl = protocol[0]+"://" + request.getHeader("Host");
  String params = "";

  boolean isDebug = false;
  String debugParam = request.getParameter("debug");
  if (debugParam != null && (debugParam.toLowerCase().equals("true") || 
                              debugParam.toLowerCase().equals("yes") || 
                              debugParam.equals("1"))) {
    isDebug = true;
  }
%>

<body ng-controller="MainCtrl">
  <span us-spinner="{color: 'red', radius:30, width:8, length: 16}" spinner-key="spinner-0"></span>
  
  <error-modal title="Error" visible="showErrorModal">
  <div class="modal-body">
    <span class="errorText">{{modal.error.message}}</span>
  </div>
  <div class="modal-footer">
    <button class="btn btn-danger" ng-click="closeError()">OK</button>
  </div>
  </error-modal>
    
  <modal title="&nbsp; Create Book" visible="showCreateModal">
    <span us-spinner spinner-key="spinner-4"></span>
    <form class="form-horizontal" name="myform" novalidate>  
      <div class="form-group has-feedback" ng-class="{'has-error': myform.isbn13.$invalid, 'has-success': myform.isbn13.$valid}">
        <label class="control-label col-sm-3" for="isbn13">ISBN13</label>
        <div class="col-sm-4">
          <input type="text" class="form-control" name="isbn13" ng-model="modal.book.isbn13"  placeholder="Enter ISBN13">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.isbn13.$valid, 'glyphicon glyphicon-remove': myform.isbn13.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group has-feedback" ng-class="{'has-error': myform.title.$invalid, 'has-success': myform.title.$valid}">
        <label class="control-label col-sm-3" for="title">Title</label>
        <div class="col-sm-8">
          <input type="text" class="form-control" name="title" required ng-model="modal.book.title"  placeholder="Book Title">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.title.$valid, 'glyphicon glyphicon-remove': myform.title.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group has-feedback" ng-class="{'has-error': myform.author.$invalid, 'has-success': myform.author.$valid}">
        <label class="control-label col-sm-3" for="author">Author</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="author" required ng-model="modal.book.author"  placeholder="Book Author">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.author.$valid, 'glyphicon glyphicon-remove': myform.author.$invalid}" ></i>
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.publisher.$invalid, 'has-success': myform.publisher.$valid}">
        <label class="control-label col-sm-3" for="publisher">Publisher</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="publisher" required ng-model="modal.book.publisher"  placeholder="Book Publisher">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.publisher.$valid, 'glyphicon glyphicon-remove': myform.publisher.$invalid}" ></i>
        </div>
      </div>
                   
      <div class="form-group has-feedback" ng-class="{'has-error': myform.isbn10.$invalid, 'has-success': myform.isbn10.$valid}">
        <label class="control-label col-sm-3" for="isbn10">ISBN10</label>
        <div class="col-sm-4">
          <input type="text" class="form-control" name="isbn10" required ng-model="modal.book.isbn10"  placeholder="Enter ISBN10">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.isbn10.$valid, 'glyphicon glyphicon-remove': myform.isbn10.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group" >
        <label class="control-label col-sm-3" for="description">Description</label>
        <div class="col-sm-8">
          <textarea class="form-control" name="description" rows="3" cols="70" ng-model="modal.book.description"  placeholder="Description">
          </textarea>
        </div>
      </div>
      
      <div class="form-group" >
        <label class="control-label col-sm-3" for="dimensions">Dimensions</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="dimensions" ng-model="modal.book.dimensions"  placeholder="Dimension" />
        </div>
      </div>

      <div class="form-group" >
        <label class="control-label col-sm-3" for="shippingWeight">Shipping Weight</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="shippingWeight" ng-model="modal.book.shippingWeight"  placeholder="Shipping Weight" />
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.language.$invalid, 'has-success': myform.language.$valid}">
        <label class="control-label col-sm-3" for="language">Language</label>
        <div class="col-sm-4">
          <select class="form-control" name="language" required ng-model="modal.book.language">
            <option value=""></option>
            <option value="arabic">Arabic</option>
            <option value="chinese">Chinese</option>
            <option value="english">English</option>
            <option value="french">French</option>
            <option value="hebrew">Hebrew</option>
            <option value="italian">Italian</option>
            <option value="japanese">Japanese</option>
            <option value="portuguese">Portuguese</option>
            <option value="spanish">Spanish</option>
          </select>
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.language.$valid, 'glyphicon glyphicon-remove': myform.language.$invalid}" ></i>
        </div>
       </div>
       
      <div class="form-group" >
        <label class="control-label col-sm-3" for="active">Is Active</label>
        <div class="col-sm-7">
          <label><input type="radio" name="active" ng-model="modal.book.active" ng-value="true">True</label>
          <label><input type="radio" name="active" ng-model="modal.book.active" ng-value="false">False</label>
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.category.$invalid, 'has-success': myform.category.$valid}">
        <label class="control-label col-sm-3" for="category">Category</label>
        <div class="col-sm-6">
          <select class="form-control" name="category" required ng-model="modal.book.category" ng-options="category.id as category.text for category in treedata | filter: greaterThan('id', 0)" >
          </select>
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.category.$valid, 'glyphicon glyphicon-remove': myform.category.$invalid}" ></i>
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.quantity.$invalid, 'has-success': myform.quantity.$valid}">
        <label class="control-label col-sm-3" for="quantity">Quantity</label>
        <div class="col-sm-4">
          <input type="number" class="form-control" name="quantity" min="0" step="1" required ng-model="modal.book.quantity"  placeholder="Enter Quantity">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.quantity.$valid, 'glyphicon glyphicon-remove': myform.quantity.$invalid}" ></i>
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.price.$invalid, 'has-success': myform.price.$valid}">
        <label class="control-label col-sm-3" for="price">Price</label>
        <div class="col-sm-4">
          <input type="number" class="form-control" name="price" min="0" step="0.01" required ng-model="modal.book.price"  placeholder="Book Price">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.price.$valid, 'glyphicon glyphicon-remove': myform.price.$invalid}" ></i>
        </div>
      </div>
      
      <div class="dialogButtons">
        <button id="create" type="submit" class="btn btn-success" ng-click="createBook()" ng-disabled="myform.$invalid" ><i class="fa fa-book"></i> Create</button>
        <button id="cancel" type="button" class="btn btn-success" ng-click="cancel()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Close</button>
      </div>
    </form>
  </modal>
  
  <modal title="&nbsp; Update Book" visible="showUpdateModal">
    <span us-spinner spinner-key="spinner-4"></span>
    <form class="form-horizontal" name="myform" novalidate>  
      <div class="form-group has-feedback" ng-class="{'has-error': myform.isbn13.$invalid, 'has-success': myform.isbn13.$valid}">
        <label class="control-label col-sm-3" for="isbn13">ISBN13</label>
        <div class="col-sm-4">
          <input type="text" class="form-control" name="isbn13" disabled ng-model="modal.book.isbn13"  placeholder="Enter ISBN13">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.isbn13.$valid, 'glyphicon glyphicon-remove': myform.isbn13.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group has-feedback" ng-class="{'has-error': myform.title.$invalid, 'has-success': myform.title.$valid}">
        <label class="control-label col-sm-3" for="title">Title</label>
        <div class="col-sm-8">
          <input type="text" class="form-control" name="title" required ng-model="modal.book.title"  placeholder="Book Title">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.title.$valid, 'glyphicon glyphicon-remove': myform.title.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group has-feedback" ng-class="{'has-error': myform.author.$invalid, 'has-success': myform.author.$valid}">
        <label class="control-label col-sm-3" for="author">Author</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="author" required ng-model="modal.book.author"  placeholder="Book Author">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.author.$valid, 'glyphicon glyphicon-remove': myform.author.$invalid}" ></i>
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.publisher.$invalid, 'has-success': myform.publisher.$valid}">
        <label class="control-label col-sm-3" for="publisher">Publisher</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="publisher" required ng-model="modal.book.publisher"  placeholder="Book Publisher">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.publisher.$valid, 'glyphicon glyphicon-remove': myform.publisher.$invalid}" ></i>
        </div>
      </div>
                   
      <div class="form-group has-feedback" ng-class="{'has-error': myform.isbn10.$invalid, 'has-success': myform.isbn10.$valid}">
        <label class="control-label col-sm-3" for="isbn10">ISBN10</label>
        <div class="col-sm-4">
          <input type="text" class="form-control" name="isbn10" required ng-model="modal.book.isbn10"  placeholder="Enter ISBN10">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.isbn10.$valid, 'glyphicon glyphicon-remove': myform.isbn10.$invalid}" ></i>
        </div>
      </div>

      <div class="form-group" >
        <label class="control-label col-sm-3" for="description">Description</label>
        <div class="col-sm-8">
          <textarea class="form-control" name="description" rows="3" cols="70" ng-model="modal.book.description"  placeholder="Description">
          </textarea>
        </div>
      </div>
      
      <div class="form-group" >
        <label class="control-label col-sm-3" for="dimensions">Dimensions</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="dimensions" ng-model="modal.book.dimensions"  placeholder="Dimensions" />
        </div>
      </div>

      <div class="form-group" >
        <label class="control-label col-sm-3" for="shippingWeight">Shipping Weight</label>
        <div class="col-sm-6">
          <input type="text" class="form-control" name="shippingWeight" ng-model="modal.book.shippingWeight"  placeholder="Shipping Weight" />
        </div>
      </div>
      
      <div class="form-group has-feedback" ng-class="{'has-error': myform.language.$invalid, 'has-success': myform.language.$valid}">
        <label class="control-label col-sm-3" for="language">Language</label>
        <div class="col-sm-4">
          <select class="form-control" name="language" required ng-model="modal.book.language">
            <option value=""></option>
            <option value="arabic">Arabic</option>
            <option value="chinese">Chinese</option>
            <option value="english">English</option>
            <option value="french">French</option>
            <option value="hebrew">Hebrew</option>
            <option value="italian">Italian</option>
            <option value="japanese">Japanese</option>
            <option value="portuguese">Portuguese</option>
            <option value="spanish">Spanish</option>
          </select>
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.language.$valid, 'glyphicon glyphicon-remove': myform.language.$invalid}" ></i>
        </div>
       </div>
       
      <div class="form-group" >
        <label class="control-label col-sm-3" for="active">Is Active</label>
        <div class="col-sm-7">
          <label><input type="radio" name="active" ng-model="modal.book.active" ng-value="true">True</label>
          <label><input type="radio" name="active" ng-model="modal.book.active" ng-value="false">False</label>
        </div>
      </div>
       
      <div class="form-group has-feedback" ng-class="{'has-error': myform.price.$invalid, 'has-success': myform.price.$valid}">
        <label class="control-label col-sm-3" for="price">Price</label>
        <div class="col-sm-4">
          <input type="number" class="form-control" name="price" min="0" step="0.01" required ng-model="modal.book.price"  placeholder="Book Price">
          <i class="form-control-feedback" ng-class="{'glyphicon glyphicon-ok': myform.price.$valid, 'glyphicon glyphicon-remove': myform.price.$invalid}" ></i>
        </div>
      </div>
      
      <div class="dialogButtons">
        <button id="update" type="submit" class="btn btn-success" ng-click="updateBook(modal.book.isbn13)" ng-disabled="myform.$invalid" ><i class="fa fa-book"></i> Update</button>
        <button id="cancel" type="button" class="btn btn-success" ng-click="cancel()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Close</button>
      </div>
    </form>
  </modal>
  
    <modal title="&nbsp; Delete Book" visible="showDeleteModal">
    <form class="form-horizontal" name="myform" novalidate>  
    
      <div class="form-group" >
        <label class="control-label col-sm-2" for="isbn13">ISBN13</label>
        <div class="col-sm-5">
          <input type="text" class="form-control" disabled name="isbn13" ng-model="modal.book.isbn13" />
        </div>
      </div>
      
      <div class="form-group" >
        <label class="control-label col-sm-2" for="title">Title</label>
        <div class="col-sm-8">
          <input type="text" class="form-control" disabled name="title" disabled ng-model="modal.book.title">
        </div> 
      </div>
      
      <div class="dialogButtons">
        <button id="delete" type="submit" class="btn btn-danger" ng-click="deleteBook(modal.row, modal.book.isbn13)" ng-disabled="myform.$invalid" ><i class="fa fa-book"></i> Delete</button>
        <button id="cancel" type="button" class="btn btn-success" ng-click="cancel()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Close</button>
      </div>
    </form>
  </modal>
  
  <div class="panel_north">
    <table style="width: 100%">
      <tr height="30px">
        <td style="width: 50%">
          <div>
            <img src="../../images/tornado_books.png" alt="Tornado Books" />
          </div>
          <div class="main_text">Book Store Lookup</div>
        </td>
        <td></td>
  
      </tr>
      <tr>
        <td>
          <table class="toptable">
            <tr>
              <td class="toptable">
                <label class="subheading_text">Please click here to see</label>
                <button name="allbooks" type="submit" class="btn btn-success btn-sm " ng-click="getAllBooks()"><i class="fa fa-book"></i> All Available Books</button>
                <button name="createbook" type="submit" class="btn btn-success btn-sm " ng-click="showCreateBook()"><i class="fa fa-plus"></i> Create Book</button>
              </td>
            </tr>
          </table>
        </td>
        <td align="right">
          <span class="date_text" current-time></span>
        </td>
      </tr>
    </table>
  </div>
  
  <div class="panel_west">
    <div class="header">2. Select Categories</div>
    <div class="sidebar">
      <span us-spinner="{top: '300'}" spinner-key="spinner-2"></span>
      <!-- <span><b>Selected Node</b> : {{node.text}} id {{node.id}} </span> -->
      <div treecontrol class="tree-classic"
        tree-model="companyList()"
        options="treeOptions"
        on-selection="processSelectedTreeNode(node)" 
        selected-node="node">
        {{node.text}}
      </div>
    </div>
  </div>
  
  <div class="panel_center">
    <div  id="mainContent">
      <span us-spinner="{top: '55'}" spinner-key="spinner-1"></span>
      <div class="header">1. Selection Criteria</div>
      <div class="div_default">
        <form name="selection_form" novalidate>
          <div class="form-inline">
            <div class="input-group input-group-sm col-sm-4">
              <!-- <label class="control-label col-sm-1">Title Search Criteria</label> -->
              <span class="input-group-addon" id="title-addon">Title Search Criteria</span>
              <input id="title" name="title" aria-describedby="title-addon" ng-model="criteria.title" class="form-control" type="text" size="30" focus-on="setFocus">
            </div>
            <button type="submit" class="btn btn-success btn-sm " ng-click="processSelectedCriteria()"><i class="fa fa-search"></i> Proceed</button> 
            <button type="submit" class="btn btn-success btn-sm " ng-click="resetForm()"><i class="fa fa-close"></i> Reset</button>
          </div> 
        </form>     
      </div>
      <div class="spacer_10"></div>
    </div>
    
    <div class="header">3. Select Available Books</div>
    <div class="content">
      <span us-spinner="{top: '300'}" spinner-key="spinner-3"></span>
      <div class="spacer_10"></div>
      <div class="inset">
        <div external-scopes="clickHandler"  ui-grid="gridOptions" ui-grid-selection class="grid" ui-grid-resize-columns ui-grid-move-columns ui-grid-pagination ui-grid-edit></div>
        <!-- <div id="grid1" ui-grid="gridOptions" ui-grid-pagination class="grid"></div> -->     
      </div>
    </div>
  </div>
  
  <div ng-element-ready="setDefaults('<%=isDebug %>', '<%=baseUrl %>')"></div>
  <div ng-element-ready="getAllCategories()"></div>
  <!-- <div class="panel_east">Hello</div> --> 
</body>
</html>

The AngularJS Application

At this point, we will begin looking at the meat of this AngularJS application. We have made use of the following open source and freely available components: angular-spinner, UI Bootstrap, Angular Tree and UI Grid.

AngularJS Spinner

We use the angular-spinner directive to give the application that professional animated spinning image when performing AJAX (Asynchronous JavaScript and XML) requests. The directive provides countless configuration options by using the usSpinnerConfigProvider.

spinner
app.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) {
    usSpinnerConfigProvider.setDefaults({
    lines: 13, // The number of lines to draw
      length: 5, // The length of each line
      width: 4, // The line thickness
      radius: 8, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      direction: 1, // 1: clockwise, -1: counterclockwise
      color: '#333', // #rgb or #rrggbb or array of colors
      speed: 1, // Rounds per second
      trail: 80, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: '50%', // Top position relative to parent
      left: '50%' // Left position relative to parent
    });
}]);

Spinner Placement in the View

You can spread the spinners anywhere in your HTML code and give each one a different key. In the following example, I will assign the spinner key as spinner-4.

<span us-spinner spinner-key=”spinner-4″></span>
However, doing so will render the spinner inactive by default. Then within the AngularJS application code, you will specify which spinner to start by using it’s key (ID), in my case we would perform the $scope.startSpin(‘spinner-4’);. In order to stop the spinner, say when the AJAX call has returned results or has failed for some reason, we would perform $scope.stopSpin(‘spinner-4’); using the spinner key as the parameter.

Tree Control Component

For this component, I actually reviewed several AngularJS tree components and ultimately settled on Angular Tree as it appeared stable and provided numerous customization options and ample documentation.

tree component

Sample JSON used in Tree Control Component

The above Tree Control component will process the JSON in the following format. It is a JSON array composed of JSON objects containing id, text and an items array.

[
  { "id": 0, "text": "All Books", "items": [] },
  { "id": 1000, "text": "Arts & Photography", "items": [] },
  { "id": 2000, "text": "Audiobooks", "items": [] },
  { "id": 3000, "text": "Biographies & Memoirs", "items": [] }
]

AngularJS TitleCase Filter

I created this filter so that when using the UI Grid component I could easily use filter option when using HTML template binding. Using the HTML template binding example below you can see how convenient a feature this can be.

{{row.entity.author | titlecase }}

app.filter('titlecase', function() {
  return function(s) {
    s = ( s === undefined || s === null ) ? '' : s;
    return s.toString().toLowerCase().replace( /\b([a-z])/g, function(ch) {
      return ch.toUpperCase();
    });
  };
});

FocusOn Directive

Since AngularJS does not have a good way to set the input field focus. I am using a directive called focus-on which will be bound to a specific input field and will listening for specific broadcast events
in order to get triggered. This code snippet was developed by Mark Rajcok and Ben Lesh. As you can see from the highlighted line below, it using the scope.$on listener to listen to specific events of a given type.

app.directive('focusOn', function() {
     return function(scope, elem, attr) {
        scope.$on(attr.focusOn, function(e) {
            elem[0].focus();
        });
     };
  });

Triggering the focus-on Event

When the user hits the RESET button I will make a call to $scope.resetForm() function and use the broadcast command to trigger a broadcast event to be dispatched.

$scope.$broadcast(‘setFocus’);

  $scope.resetForm = function () {
    $scope.criteria = {};
    $scope.criteria.title = '';
    $scope.gridOptions.data = [];
    $scope.$broadcast('setFocus');
  };

AngularJS AjaxService as a Service

In my application I am creating an ajaxService using AngularJS services. Services in AngularJS are constructed as singleton objects and each component making use of that service gets a reference to the same single instance of that object. The other benefit of using services is that fact that they are not instantiated until they are called for the first time, this is called Lazy-Instantiation.

The other reason why I used services was because prior to me refactoring the code, I was actually using multiple controllers in this application. When you create a service you are able to easily share it across multiple controllers without any issues.

In this example, you see that there are two methods associated with the ajaxService. We are making use of two different ways to call the ajaxService, that is using getData and postData.

app.service('ajaxService', function($http) {
  this.getData = function(URL, ajaxMethod, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService GET...");
    console.log("Connection using URL=[" + restURL + "], 
                                       Method=[" + ajaxMethod + "]");

    return $http({
      method: ajaxMethod,
      url: restURL,
    });
  };
   
  this.postData = function(URL, ajaxMethod, jsonData, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService POST...");
    console.log("Connection using URL=[" + restURL + "], 
                                       Method=[" + ajaxMethod + "]");
    
    return $http({
      method: ajaxMethod,
      url: restURL,
      headers: {'Content-Type': 'application/json'},
      data: jsonData,
    });
  };
});

Calling our ajaxService from our Code

Our AngularJS application makes use of Promises as can be seen by the use of the .then(onSuccess, onError). We use promises to specify what operations take place when the REST API operations finally succeed or fail.

Promises are a core feature in AngularJS. The implementation of promises/deferred objects in AngularJS was inspired by Kris Kowal’s Q.

$scope.createBook = function () {
  console.log('Inside createBook...');
  $scope.startSpin('spinner-4');

  // Removing some code for the succinctness ...

  createUrl = createBookUrl + "isbn13=" + $scope.modal.book.isbn13;
  createUrl += '&etc=' + new Date().getTime();

  function onSuccess(response) {
    console.log("+++++createBook SUCCESS++++++");
    if (response.data.success == false) {
      $scope.showErrorModalWindow('Error!', response.data.message);
    } else {
      $scope.showCreateModal = false;       
    }
    $scope.stopSpin('spinner-4');
  };
  
  function onError(response) {
    console.log("Inside createBook error condition...");
    $scope.stopSpin('spinner-4');
    $scope.showErrorModalWindow('Error!', response.data.message);
  };

  //----MAKE AJAX REQUEST CALL to POST DATA----
  ajaxService.postData(createUrl, 'POST', $scope.modal.book, '')
       .then(onSuccess, onError);    
};

AngularJS UI-Grid Features and Functionality

Angular UI Grid provides so many customization features that are important to UI developers. These include sorting, filtering, column pinning, grouping, edit in place, column resizing, column reordering, expandable rows and pagination. Setting up these customization options are done via the gridOptions object.

Setting up AngularJS UI-Grid Options

Using gridOptions object we are able to enable Filtering, in-cell editing, pagination, sorting, row selection, column resizing, and pagination page sizes.

$scope.gridOptions = {
    enableFiltering: true,
    enableCellEditOnFocus: false,
    enablePaginationControls: true,
    enableSorting: true,
    enableRowSelection: true,
    enableRowHeaderSelection: false,
    enableColumnResizing: true,
    paginationPageSizes: [10, 12, 15, 18],
    paginationPageSize: 18,
  };

Setting up AngularJS UI-Grid Column Definitions

Once you have set up the global grid options you you need to fine tune the options column by column. This will enable you to define column name, define column widths, define minimum and maximum column sizes, set up the display name for the column and configure cell templates to fully customize the column’s fields (which includes adding links and icons) and adding on-click event handlers.

$scope.gridOptions.columnDefs = [
  { name: 'isbn13', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate:'<div class="editBook1"><a class="editBook" href="" ng-click="$event.stopPropagation(); grid.appScope.editBook(row.entity.isbn13, row);">'
      + '{{row.entity[col.field] | uppercase }}&nbsp; <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a></div>',
    cellClass: 'gridField', 
    displayName: 'ISBN13', 
    width: 140, 
    maxWidth: 160, 
    minWidth: 130, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'title', 
    headerCellClass: $scope.highlightFilteredHeader , 
    cellTemplate: '<div class="padded">{{row.entity.title}}</div>',
    cellClass: 'gridField', 
    displayName: 'Title', 
    width: 250, 
    maxWidth: 480, 
    minWidth: 220, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'author',
    headerCellClass: $scope.highlightFilteredHeader,
    cellTemplate: '<div class="centered">{{row.entity.author | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 160, 
    maxWidth: 200, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'publisher', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{row.entity.publisher | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 90, 
    maxWidth: 200, 
    minWidth: 90, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'isbn10', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="centered">{{row.entity.isbn10}}</div>',
    cellClass: 'gridField', 
    displayName: 'ISBN10', 
    width: 80, 
    maxWidth: 140, 
    minWidth: 70, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'dimensions', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{ row.entity.dimensions }}</div>',
    cellClass: 'gridField', 
    width: 130, 
    maxWidth: 220, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'active', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Is Active', 
    width: 70, 
    maxWidth: 120, 
    minWidth: 60, 
    enableHiding: false, 
    //type: 'boolean', 
    enableCellEdit: false, 
    cellTemplate: '<div class="isActive">'
      + '<a class="isActive" href="" ng-click="$event.stopPropagation(); row.entity.active=grid.appScope.toggleIsActive(row, row.entity.active);">' 
      + '<span ng-class="row.entity[col.field] ? \'checkIcon\' : \'xIcon\'">'
      + '<span ng-if="row.entity[col.field]">Yes</span>'
      + '<span ng-if="!row.entity[col.field]">No</span>'
      + '</span></a></div>'},
  { name: 'price', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Price', 
    width: 60, 
    maxWidth: 120, 
    minWidth: 50, 
    enableHiding: false, 
    enableCellEdit: false
  }, 
  { name: 'delete', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Del', 
    width: 40, 
    maxWidth: 40, 
    minWidth: 40, 
    enableHiding: false, 
    enableCellEdit: false, 
    cellTemplate: '<div class="centered"><button id="cancel" type="button" class="btn btn-danger btn-xs" ng-click="$event.stopPropagation(); grid.appScope.deleteModal(row, row.entity.isbn13, row.entity.title)"><span class="glyphicon glyphicon-remove"></span></button></div>'    
  }
];

The AngularJS Application Complete Code (app.js)

var app = angular.module('app', ['ui.bootstrap', 'angularSpinner', 'myTimeModule', 'treeControl', 'ui.grid', 'ui.grid.selection', 'ui.grid.edit', 'ui.grid.cellNav', 'ui.grid.pagination', 'ui.grid.resizeColumns', 'ui.grid.moveColumns']);

app.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) {
    usSpinnerConfigProvider.setDefaults({
    lines: 13, // The number of lines to draw
      length: 5, // The length of each line
      width: 4, // The line thickness
      radius: 8, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      direction: 1, // 1: clockwise, -1: counterclockwise
      color: '#333', // #rgb or #rrggbb or array of colors
      speed: 1, // Rounds per second
      trail: 80, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: '50%', // Top position relative to parent
      left: '50%' // Left position relative to parent
    });
}]);

app.filter('titlecase', function() {
    return function(s) {
        s = ( s === undefined || s === null ) ? '' : s;
        return s.toString().toLowerCase().replace( /\b([a-z])/g, function(ch) {
            return ch.toUpperCase();
        });
    };
});

app.directive('focusOn', function() {
     return function(scope, elem, attr) {
        scope.$on(attr.focusOn, function(e) {
            elem[0].focus();
        });
     };
  });

app.directive('modal', function () {
    return {
      template: '<div class="modal fade">' + 
          '<div class="modal-dialog">' + 
            '<div class="modal-content">' + 
              '<div class="modal-header">' + 
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="false">&times;</button>' +
                '<h4 class="modal-title">'+
                '<span class="glyphicon glyphicon-star" aria-hidden="true"></span>' + 
                '  {{ title }}</h4>' + 
              '</div>' + 
              '<div class="modal-body" ng-transclude></div>' + 
            '</div>' + 
          '</div>' + 
        '</div>',
      restrict: 'E',
      transclude: true,
      replace:true,
      scope:true,
      link: function postLink(scope, element, attrs) {
        scope.title = attrs.title;

        scope.$watch(attrs.visible, function(value){
          if(value == true) {
            $(element).modal('show');
            scope.tt_isOpen = false;
          } else
            $(element).modal('hide');
        });

        $(element).on('shown.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = true;
          });
        });

        $(element).on('hidden.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = false;
          });
        });
      }
    };
  });

app.directive('errorModal', function () {
    return {
      template: '<div class="modal fade">' + 
          '<div class="modal-dialog">' + 
            '<div class="modal-content">' + 
              '<div class="modal-header-error">' + 
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="false">&times;</button>' +
                '<h4 class="modal-title-error">'+
                '<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>' + 
                '&nbsp;&nbsp;{{ modal.error.title }}</h4>' + 
              '</div>' + 
            '<div class="modal-body" ng-transclude></div>' + 
            '</div>' + 
          '</div>' + 
        '</div>',
      restrict: 'E',
      transclude: true,
      replace:true,
      scope:true,
      link: function postLink(scope, element, attrs) {
        scope.title = attrs.title;

        scope.$watch(attrs.visible, function(value){
          if(value == true) {
            $(element).modal('show');
            scope.tt_isOpen = false;
          } else
            $(element).modal('hide');
        });

        $(element).on('shown.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = true;
          });
        });

        $(element).on('hidden.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = false;
          });
        });
      }
    };
  });

app.service('ajaxService', function($http) {
  this.getData = function(URL, ajaxMethod, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService GET...");
    console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]");

    return $http({
      method: ajaxMethod,
      url: restURL,
    });
   };
   
  this.postData = function(URL, ajaxMethod, jsonData, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService POST...");
    console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]");
    
    return $http({
      method: ajaxMethod,
      url: restURL,
      headers: {'Content-Type': 'application/json'},
      data: jsonData,
    });
  };
});

app.directive('ngElementReady', [function() {
    return {
        priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
        restrict: "A",
        link: function($scope, $element, $attributes) {
            $scope.$eval($attributes.ngElementReady); // execute the expression in the attribute.
        }
    };
}]);

var myTimeModule = angular.module('myTimeModule', [])
// Register the 'myCurrentTime' directive factory method.
// We inject $timeout and dateFilter service since the factory method is DI.
.directive('currentTime', function($timeout, dateFilter) {
  // return the directive link function. (compile function not needed)
  return function(scope, element, attrs) {
    var timeoutId = 0; // timeoutId, so that we can cancel the time updates

    // used to update the UI
    function updateTime() {
      element.text(dateFilter(new Date(), 'MMM d, yyyy h:mm:ss a'));
    }

    // schedule update in one second
    function updateLater() {
      // save the timeoutId for canceling
      timeoutId = $timeout(function() {
        updateTime(); // update DOM
        updateLater(); // schedule another update
      }, 1000);
    }

    // listen on DOM destroy (removal) event, and cancel the next UI update
    // to prevent updating time ofter the DOM element was removed.
    element.bind('$destroy', function() {
      $timeout.cancel(timeoutId);
    });

    updateLater(); // kick off the UI update process.
  };
});

/* -----------------------------------------------------------------------
** MAIN CONTROLLER  
*-------------------------------------------------------------------------*/
app.controller('MainCtrl', function ($scope, $rootScope, $http, $log, $timeout, $modal, $filter, uiGridConstants, ajaxService, usSpinnerService) {
  
 
  $scope.modal = {};
  $scope.modal.error = {};
  $scope.modal.book = {};
  
  
  $scope.showAddModal = false;
  $scope.showUpdateModal = false;
  
  $scope.startSpin = function(key) {
    usSpinnerService.spin(key);
  };
  
  $scope.stopSpin = function(key) {
    usSpinnerService.stop(key);
  };
 
  $scope.setTitleCase = function(input) {
    if (input != null ) {
      return input.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
    }
  };

  $scope.setLowerCase = function(input) {
    if (input != null ) {
      return input.replace(/\w\S*/g, function(txt){return txt.toLowerCase();});
    }
  };

  $scope.setUpperCase = function(input) {
      if (input != null ) {
      return input.replace(/\w\S*/g, function(txt){return txt.toUpperCase();});
      }
    };
  
  $scope.currentTimeMillis = function(ts) {
    var date = new Date().getTime();
    return date;
  };
  
  $scope.setDefaults = function(debugFlag, baseUrl) {
    categoryLoadUrl   = baseUrl + "/AngularUIGrid/rest/findbycategory?";
    titleLoadUrl    = baseUrl + "/AngularUIGrid/rest/findbytitle?";
    getAllBooksUrl  = baseUrl + "/AngularUIGrid/rest/books";
    getBookByISBNUrl  = baseUrl + "/AngularUIGrid/rest/getbookbyisbn?";
    createBookUrl   = baseUrl + "/AngularUIGrid/rest/book/add?";
    editBookUrl   = baseUrl + "/AngularUIGrid/rest/book/update?";
    allCategoriesUrl  = baseUrl + "/AngularUIGrid/rest/categories?";
    
    console.log("Setting Defaults");
    console.log("DebugFlag..........: " + debugFlag);
    console.log("categoryLoadUrl....: " + categoryLoadUrl);
    console.log("titleLoadUrl.......: " + titleLoadUrl);
    console.log("createBookUrl......: " + createBookUrl);
    console.log("editBookUrl........: " + editBookUrl);
    console.log("getAllBooksUrl.....: " + getAllBooksUrl);
    console.log("allCategoriesUrl...: " + allCategoriesUrl);
    
    $scope.debugFlag = debugFlag;
    $scope.baseUrl = baseUrl;
    $scope.criteria = {};
    $scope.criteria.title = '';
    $scope.showCreateModal = false;
    $scope.showUpdateModal = false;
    $scope.showDeleteModal = false;
    $scope.showErrorModal = false;

  };

  $scope.$on('$viewContentLoaded', function() {
    console.log("viewContentLoaded event triggered...");
    loadUserDefaults();
  });
  
  $scope.showCreateBook= function () {
    $scope.modal = {};
    $scope.modal.book = {};
    $scope.showCreateModal = true;  
  }
  
  $scope.showErrorModalWindow = function(title, message) {
    $scope.modal.error = {};
    $scope.modal.error.title = title;
    $scope.modal.error.message = message;
    $scope.showErrorModal = true;
  }
  
  $scope.getAllBooks = function () {
    var url = getAllBooksUrl;
  
    $scope.startSpin('spinner-3');
    console.log("Inside getAllBooks " + url);

  function onSuccess(response) {
    console.log("+++++getAllBooks SUCCESS++++++");
    if ($scope.debugFlag == 'true') {
      console.log("Inside getAllBooks response..." + JSON.stringify(response.data));
    } else {
      console.log("Inside getAllBooks response...(XML response is being skipped, debug=false)");
    }
    if (response.data.success != false) { 
      $scope.gridOptions.data = response.data;
    } else {
      $scope.gridOptions.data = [];
    }
    $scope.stopSpin('spinner-3');
  };
    
  function onError(response) {
    console.log("-------getAllBooks FAILED-------");
    $scope.stopSpin('spinner-3');
    console.log("Inside getAllBooks error condition...");
  };
    
    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(url, 'GET', '').then(onSuccess, onError);
    
  };
  
  //---Cancel Modal Dialog Window---
  $scope.cancel = function () {
    console.log('Closing Modal Dialog Window...');
    $scope.showCreateModal = false;
    $scope.showUpdateModal = false;
    $scope.showDeleteModal = false;
  };
  
  //---Close Error Modal Window---
  $scope.closeError = function () {
    console.log('Closing Error Modal Window...');
    $scope.showErrorModal = false;
  };

  $scope.highlightFilteredHeader = function( row, rowRenderIndex, col, colRenderIndex ) {
    if( col.filters[0].term ) {
      return 'header_filtered';
    } else {
      return 'header_text';
    }
  };
  
  $scope.gridOptions = { 
    enableFiltering: true,
    enableCellEditOnFocus: false,
    enablePaginationControls: true,
    enableSorting: true,
    enableRowSelection: true,
    enableRowHeaderSelection: false,
    enableColumnResizing: true,
    paginationPageSizes: [10, 12, 15, 18],  //18
    paginationPageSize: 18,       //18
  };

  $scope.gridOptions.onRegisterApi = function(gridApi){
    //set gridApi on scope
    $scope.gridApi = gridApi;
    gridApi.edit.on.afterCellEdit($scope,function(rowEntity, colDef, newValue, oldValue){
      console.log('edited row uid:' + rowEntity.isbn13 + ' Column:' + colDef.name + ' newValue:' + newValue + ' oldValue:' + oldValue );
      $scope.$apply();
    });
  };
    
  $scope.gridOptions.multiSelect = false;

  //---Create Modal Dialog Window---
  $scope.createBook = function () {
    console.log('Inside createBook...');
    $scope.startSpin('spinner-4');
    
    
    createUrl = createBookUrl + "isbn13=" + $scope.modal.book.isbn13;
    createUrl += '&etc=' + new Date().getTime();

    console.log("createBook..: " + createUrl);
    
    $scope.modal.book.id = $scope.modal.book.isbn13;
    console.log("$scope.modal.book.id..........: " + $scope.modal.book.id);
    console.log("$scope.modal.book.isbn13..........: " + $scope.modal.book.isbn13);
    console.log("$scope.modal.book.title...........: " + $scope.modal.book.title);
    console.log("$scope.modal.book.author..........: " + $scope.modal.book.author);
    console.log("$scope.modal.book.publisher.......: " + $scope.modal.book.publisher);
    console.log("$scope.modal.book.isbn10..........: " + $scope.modal.book.isbn10);
    console.log("$scope.modal.book.description.....: " + $scope.modal.book.description);
    console.log("$scope.modal.book.dimensions......: " + $scope.modal.book.dimensions);
    console.log("$scope.modal.book.shippingWeight..: " + $scope.modal.book.shippingWeight);
    console.log("$scope.modal.book.language........: " + $scope.modal.book.language);
    console.log("$scope.modal.book.category........: " + $scope.modal.book.category);
    console.log("$scope.modal.book.active..........: " + $scope.modal.book.active);
    console.log("$scope.modal.book.quantity........: " + $scope.modal.book.quantity);
    console.log("$scope.modal.book.price...........: " + $scope.modal.book.price);

    if ($scope.debugFlag == 'true') {
      console.log("$scope.modal.book...: " + angular.toJson($scope.modal.book, true));
    }
    
    
     function onSuccess(response) {
      console.log("+++++createBook SUCCESS++++++");

      if ($scope.debugFlag == 'true') {
        console.log("Inside createBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside createBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.showCreateModal = false;       
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------createBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside createBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.postData(createUrl, 'POST', $scope.modal.book, '').then(onSuccess, onError);
    
  };
  
  //---Update Modal Dialog Window---
  $scope.updateBook = function (isbn13) {
    console.log('Inside updateBook...' + isbn13);
    $scope.startSpin('spinner-4');
    
    updateBookUrl = editBookUrl + "isbn13=" + isbn13;
    console.log("updateBook..: " + updateBookUrl);
    if ($scope.debugFlag == 'true') {
      console.log("$scope.modal.book...: " + angular.toJson($scope.modal.book, true));
    }
    
    function onSuccess(response) {
      console.log("+++++updateBook SUCCESS++++++");
      $scope.modal.row.entity.isbn13 = $scope.modal.book.isbn13;
      $scope.modal.row.entity.title = $scope.modal.book.title;
      $scope.modal.row.entity.author = $scope.modal.book.author;
      $scope.modal.row.entity.publisher = $scope.modal.book.publisher;
      $scope.modal.row.entity.isbn10 = $scope.modal.book.isbn10;
      $scope.modal.row.entity.dimensions = $scope.modal.book.dimensions;
      $scope.modal.row.entity.active = $scope.modal.book.active;
      $scope.modal.row.entity.price = $scope.modal.book.price;

      if ($scope.debugFlag == 'true') {
        console.log("Inside updateBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside updateBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      }
      $scope.stopSpin('spinner-4');
      $scope.showUpdateModal = false;
    };
  
    function onError(response) {
      console.log("-------updateBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside updateBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.postData(updateBookUrl, 'PUT', $scope.modal.book, '').then(onSuccess, onError);
    
  };

  $scope.editBook = function(isbn13, row) {
    $scope.startSpin('spinner-4');

    console.log('Inside editBook: ' + isbn13);
    
    getBookURL = getBookByISBNUrl;
    getBookURL += "isbn13=" + isbn13;
    getBookURL += '&etc=' + new Date().getTime();
    
    console.log("getBookURL.........: " + getBookURL);
    
    
    //----- Get Book by ISBN13------
    
    function onSuccess(response) {
      console.log("+++++editBook SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside editBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside editBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.modal.row = row;
        $scope.modal.book.id = response.data.isbn13;
        $scope.modal.book.isbn13 = response.data.isbn13;
        $scope.modal.book.title = response.data.title;
        $scope.modal.book.author = response.data.author;
        $scope.modal.book.publisher = response.data.publisher;
        $scope.modal.book.isbn10 = response.data.isbn10;
        $scope.modal.book.dimensions = response.data.dimensions;
        $scope.modal.book.quantity = response.data.quantity;
        $scope.modal.book.description = response.data.description;
        $scope.modal.book.active = response.data.active;
        $scope.modal.book.shippingWeight = response.data.shippingWeight;
        $scope.modal.book.language = response.data.language;
        $scope.modal.book.price = response.data.price;
        $scope.showUpdateModal = true;
        if ($scope.debugFlag == 'true') {
          console.log(angular.toJson($scope.modal.book, true));
        }
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------editBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside editBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.getData(getBookURL, 'GET', '').then(onSuccess, onError);
        
  };
  
  $scope.deleteModal = function (row, isbn13, title) {
    console.log('Inside of deleteModal, isbn13 = ' + isbn13);
    $scope.modal = {};
    $scope.modal.book = {};
    
    $scope.modal.row = row;
    $scope.modal.book.isbn13 = isbn13;
    $scope.modal.book.title = title;
    $scope.showDeleteModal = true;
  };
  
  $scope.deleteBook = function (row, isbn13) {
    console.log('Inside of deleteBook, isbn13 = ' + isbn13);
    
  };
  
  $scope.deleteBookFromGrid = function (row, isbn13) {
    var index = $scope.gridOptions.data.indexOf(row.entity);
    $scope.gridOptions.data.splice(index, 1);
    $scope.showDeleteModal = false;
  };  

  
  $scope.updateIsActive = function (isbn13, row, value) {
    var updateBookUrl; 
    
    value = !value;
    $scope.modal.book.active = value;
    
    updateBookUrl = editBookUrl + "isbn13=" +isbn13;
    console.log('Inside of updateIsActive ' + updateBookUrl + ', value = ' + value);
    $scope.startSpin('spinner-3');

    function onSuccess(response) {
      console.log("+++++updateIsActive SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside updateIsActive response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside updateIsActive response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        // Reset values in callback as there was a data problem on back-end...
        value = !value;
        row.entity.active = value; 
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        row.entity.active = value; 
      }
      $scope.stopSpin('spinner-3');
    };
  
    function onError(response) {
      console.log("-------updateIsActive FAILED-------");
      $scope.stopSpin('spinner-3');
      console.log("Inside updateIsActive error condition...");
    };

    if ($scope.debugFlag == 'true') {
      console.log(angular.toJson($scope.modal.book, true));
    }
    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.postData(updateBookUrl, 'PUT', $scope.modal.book, '').then(onSuccess, onError);   
  };
  
  $scope.toggleIsActive = function (row, value) {
    var isbn13;
    
    $scope.startSpin('spinner-4');

    isbn13 = row.entity.isbn13;
    console.log('Inside toggleIsActive: ' + isbn13);
    
    getBookURL = getBookByISBNUrl;
    getBookURL += "isbn13=" + isbn13;
    getBookURL += '&etc=' + new Date().getTime();
    
    console.log("toggleIsActive.........: " + getBookURL);
    
    
    //----- Get Book by ISBN13------
    
    function onSuccess(response) {
      console.log("+++++toggleIsActive SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside toggleIsActive response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside toggleIsActive response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.modal.row = row;
        $scope.modal.book.id = response.data.isbn13;
        $scope.modal.book.isbn13 = response.data.isbn13;
        $scope.modal.book.title = response.data.title;
        $scope.modal.book.author = response.data.author;
        $scope.modal.book.publisher = response.data.publisher;
        $scope.modal.book.isbn10 = response.data.isbn10;
        $scope.modal.book.dimensions = response.data.dimensions;
        $scope.modal.book.quantity = response.data.quantity;
        $scope.modal.book.description = response.data.description;
        $scope.modal.book.active = response.data.active;
        $scope.modal.book.shippingWeight = response.data.shippingWeight;
        $scope.modal.book.language = response.data.language;
        $scope.modal.book.price = response.data.price;
        if ($scope.debugFlag == 'true') {
          console.log(angular.toJson($scope.modal.book, true));
        }
        $scope.updateIsActive(isbn13, row, value);
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------toggleIsActive FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside toggleIsActive error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.getData(getBookURL, 'GET', '').then(onSuccess, onError);
  }
  
  $scope.gridOptions.columnDefs = [
  { name: 'isbn13', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate:'<div class="editBook1"><a class="editBook" href="" ng-click="$event.stopPropagation(); grid.appScope.editBook(row.entity.isbn13, row);">'
      + '{{row.entity[col.field] | uppercase }}&nbsp; <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a></div>',
    cellClass: 'gridField', 
    displayName: 'ISBN13', 
    width: 140, 
    maxWidth: 160, 
    minWidth: 130, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'title', 
    headerCellClass: $scope.highlightFilteredHeader , 
    cellTemplate: '<div class="padded">{{row.entity.title}}</div>',
    cellClass: 'gridField', 
    displayName: 'Title', 
    width: 250, 
    maxWidth: 480, 
    minWidth: 220, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'author',
    headerCellClass: $scope.highlightFilteredHeader,
    cellTemplate: '<div class="centered">{{row.entity.author | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 160, 
    maxWidth: 200, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'publisher', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{row.entity.publisher | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 90, 
    maxWidth: 200, 
    minWidth: 90, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'isbn10', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="centered">{{row.entity.isbn10}}</div>',
    cellClass: 'gridField', 
    displayName: 'ISBN10', 
    width: 80, 
    maxWidth: 140, 
    minWidth: 70, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'dimensions', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{ row.entity.dimensions }}</div>',
    cellClass: 'gridField', 
    width: 130, 
    maxWidth: 220, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'active', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Is Active', 
    width: 70, 
    maxWidth: 120, 
    minWidth: 60, 
    enableHiding: false, 
    //type: 'boolean', 
    enableCellEdit: false, 
    cellTemplate: '<div class="isActive">'
      + '<a class="isActive" href="" ng-click="$event.stopPropagation(); row.entity.active=grid.appScope.toggleIsActive(row, row.entity.active);">' 
      + '<span ng-class="row.entity[col.field] ? \'checkIcon\' : \'xIcon\'">'
      + '<span ng-if="row.entity[col.field]">Yes</span>'
      + '<span ng-if="!row.entity[col.field]">No</span>'
      + '</span></a></div>'},
  { name: 'price', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Price', 
    width: 60, 
    maxWidth: 120, 
    minWidth: 50, 
    enableHiding: false, 
    enableCellEdit: false
  }, 
  { name: 'delete', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Del', 
    width: 40, 
    maxWidth: 40, 
    minWidth: 40, 
    enableHiding: false, 
    enableCellEdit: false, 
    cellTemplate: '<div class="centered"><button id="cancel" type="button" class="btn btn-danger btn-xs" ng-click="$event.stopPropagation(); grid.appScope.deleteModal(row, row.entity.isbn13, row.entity.title)"><span class="glyphicon glyphicon-remove"></span></button></div>'    
  }
  ];

  $scope.gridOptions.data = [];
    

/* -----------------------------------------------------------------------
* CATEGORIES TREE  
--------------------------------------------------------------------------*/

  $scope.criteria = {};
  $scope.reverse = true;
  
  $scope.treeOptions = {
      nodeChildren: "item",
      dirSelectable: true
  };
  
  $scope.defaultData = [  { "text" : "All Books", "id" : "all", "item" : [] },
                       { "text" : "Books #1", "id" : "1", "item" : [
                         { "text" : "Books #2", "id" : "2", "item" : [] },
                         { "text" : "Books #3", "id" : "3", "item" : [] },
                         { "text" : "Books #4", "id" : "4", "item" : [] },
                         { "text" : "Books #5", "id" : "5", "item" : [] }
                       ]},
                       { "text" : "Books #6", "id" : "6", "item" : [] },
                       { "text" : "Books #7", "id" : "7", "item" : [] }
                     ];
  
  $scope.companyList = function() {
    return $scope.treedata;
  };

  $scope.node = function(num) {
    $scope.treedata = num;
    return $scope.treedata;
  };
  
  $scope.clearNode = function() {
    $scope.node = undefined;
  };
  
  $scope.getDefaultTreeData = function() {
    return $scope.defaultData;
  };
  
  $scope.processSelectedTreeNode = function (node) {
    var gridUrl, addl_params;
    
    
    if (node.id == '0') {
      gridUrl = titleLoadUrl;
      addl_params = 'title=' + $scope.criteria.title
        + '&etc=' + new Date().getTime();
    } else {
      gridUrl = categoryLoadUrl;
      addl_params = '&category=' + node.id 
        + '&title=' + $scope.criteria.title
        + '&etc=' + new Date().getTime();
    }
    $scope.startSpin('spinner-3');
    
    console.log("Inside processSelectedTreeNode Selected node is " + node.id);

    function onSuccess(response) {
      console.log("+++++processSelectedTreeNode SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside processSelectedTreeNode response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside processSelectedTreeNode response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success != false) { 
        console.log("response 'success'.."); 
        $scope.gridOptions.data = response.data;
      } else {
        console.log("response came back with 404!!!!");         
        $scope.gridOptions.data = [];
        $scope.showErrorModalWindow('Error!', response.data.message);
      }
      $scope.stopSpin('spinner-3');
    };
    
    function onError(response) {
      console.log("-------processSelectedTreeNode FAILED-------");
      $scope.stopSpin('spinner-3');
      console.log("Inside processSelectedTreeNode error condition...");
    };
    
    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(gridUrl, 'GET', addl_params).then(onSuccess, onError);      
  };

  $scope.treeUnset = function() {
    console.log("Inside of treeUnset...");
    $scope.clearNode();
  };
  
  $scope.resetForm = function () {
    $scope.criteria = {};
    $scope.criteria.title = '';
    $scope.gridOptions.data = [];
    $scope.$broadcast('setFocus');
  };

  $scope.greaterThan = function(id, val){
      return function(category){
        return category[id] > val;
      }
  };
  
  $scope.getAllCategories = function () {
    var url = allCategoriesUrl;

        console.log("Inside getAllCategories...");
      
    function onSuccess(response) {
      if ($scope.debugFlag == 'true') {
        console.log("Inside getAllCategories response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside getAllCategories response...(XML response is being skipped, debug=false)");
      }
      $scope.treedata = response.data;
      $rootScope.treedata = response.data;
      //console.log("$rootScope.treedata: " + JSON.stringify($rootScope.treedata));
      $scope.stopSpin('spinner-2');
    };
    
    function onError(response) {
      $scope.stopSpin('spinner-2');
      console.log("Inside getAllCategories error condition...");
    };
    
    $scope.gridOptions.data = [];
    // Clear TreeView Selected Node
    $scope.treeUnset();
    $rootScope.treedata = [];
    $scope.treedata = [];
    
    $scope.startSpin('spinner-2');
    console.log("Inside getAllCategories..." + url);
    var addl_params ='etc='+new Date().getTime();
    
var app = angular.module('app', ['ui.bootstrap', 'angularSpinner', 'myTimeModule', 'treeControl', 'ui.grid', 'ui.grid.selection', 'ui.grid.edit', 'ui.grid.cellNav', 'ui.grid.pagination', 'ui.grid.resizeColumns', 'ui.grid.moveColumns']);

app.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) {
    usSpinnerConfigProvider.setDefaults({
			lines: 13, // The number of lines to draw
			length: 5, // The length of each line
			width: 4, // The line thickness
			radius: 8, // The radius of the inner circle
			corners: 1, // Corner roundness (0..1)
			rotate: 0, // The rotation offset
			direction: 1, // 1: clockwise, -1: counterclockwise
			color: '#333', // #rgb or #rrggbb or array of colors
			speed: 1, // Rounds per second
			trail: 80, // Afterglow percentage
			shadow: false, // Whether to render a shadow
			hwaccel: false, // Whether to use hardware acceleration
			className: 'spinner', // The CSS class to assign to the spinner
			zIndex: 2e9, // The z-index (defaults to 2000000000)
			top: '50%', // Top position relative to parent
			left: '50%' // Left position relative to parent
    });
}]);

app.filter('titlecase', function() {
  return function(s) {
    s = ( s === undefined || s === null ) ? '' : s;
    return s.toString().toLowerCase().replace( /\b([a-z])/g, function(ch) {
      return ch.toUpperCase();
    });
  };
});

app.directive('focusOn', function() {
  return function(scope, elem, attr) {
		scope.$on(attr.focusOn, function(e) {
			elem[0].focus();
		});
  };
});

app.directive('modal', function () {
    return {
      template: '<div id="myModal" class="modal fade">' + 
          '<div class="modal-dialog">' + 
            '<div class="modal-content">' + 
              '<div class="modal-header">' + 
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="false">&times;</button>' +
                '<h4 class="modal-title">'+
                '<span class="glyphicon glyphicon-star" aria-hidden="true"></span>' + 
                '  {{ title }}</h4>' + 
              '</div>' + 
              '<div class="modal-body" ng-transclude></div>' + 
            '</div>' + 
          '</div>' + 
        '</div>',
      restrict: 'E',
      transclude: true,
      replace:true,
      scope:true,
      link: function postLink(scope, element, attrs) {
        scope.title = attrs.title;

        scope.$watch(attrs.visible, function(value){
          if(value == true) {
            $(element).modal('show');
            scope.tt_isOpen = false;
          } else
            $(element).modal('hide');
        });

        $(element).on('shown.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = true;
          });
        });

        $(element).on('hidden.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = false;
          });
        });
      }
    };
  });

app.directive('errorModal', function () {
    return {
      template: '<div id="errorModal" class="modal fade">' + 
          '<div class="modal-dialog">' + 
            '<div class="modal-content">' + 
              '<div class="modal-header-error">' + 
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '<h4 class="modal-title-error">'+
                '<span class="glyphicon glyphicon-alert" aria-hidden="true"></span>' + 
                '&nbsp;&nbsp;{{ modal.error.title }}</h4>' + 
              '</div>' + 
            '<div class="modal-body" ng-transclude></div>' + 
            '</div>' + 
          '</div>' + 
        '</div>',
      restrict: 'E',
      transclude: true,
      replace:true,
      scope:true,
      link: function postLink(scope, element, attrs) {
        scope.title = attrs.title;

        scope.$watch(attrs.visible, function(value){
          if(value == true) {
            $(element).modal('show');
            scope.tt_isOpen = false;
          } else
            $(element).modal('hide');
        });

        $(element).on('shown.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = true;
          });
        });

        $(element).on('hidden.bs.modal', function(){
          scope.$apply(function(){
            scope.$parent[attrs.visible] = false;
          });
        });
      }
    };
  });

app.service('ajaxService', function($http) {
  this.getData = function(URL, ajaxMethod, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService GET...");
    console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]");

		return $http({
			method: ajaxMethod,
			url: restURL,
		});
	};
   
  this.postData = function(URL, ajaxMethod, jsonData, ajaxParams) {
    var restURL = URL + ajaxParams;
    console.log("Inside ajaxService POST...");
    console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]");
	
		return $http({
				method: ajaxMethod,
				url: restURL,
				headers: {'Content-Type': 'application/json'},
				data: jsonData,
		 });
    
  };
});

app.directive('ngElementReady', [function() {
    return {
        priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
        restrict: "A",
        link: function($scope, $element, $attributes) {
            $scope.$eval($attributes.ngElementReady); // execute the expression in the attribute.
        }
    };
}]);

var myTimeModule = angular.module('myTimeModule', [])
// Register the 'myCurrentTime' directive factory method.
// We inject $timeout and dateFilter service since the factory method is DI.
.directive('currentTime', function($timeout, dateFilter) {
  // return the directive link function. (compile function not needed)
  return function(scope, element, attrs) {
    var timeoutId = 0; // timeoutId, so that we can cancel the time updates

    // used to update the UI
    function updateTime() {
      element.text(dateFilter(new Date(), 'MMM d, yyyy h:mm:ss a'));
    }

    // schedule update in one second
    function updateLater() {
      // save the timeoutId for canceling
      timeoutId = $timeout(function() {
        updateTime(); // update DOM
        updateLater(); // schedule another update
      }, 1000);
    }

    // listen on DOM destroy (removal) event, and cancel the next UI update
    // to prevent updating time ofter the DOM element was removed.
    element.bind('$destroy', function() {
      $timeout.cancel(timeoutId);
    });

    updateLater(); // kick off the UI update process.
  };
});

/* -----------------------------------------------------------------------
** MAIN CONTROLLER  
*-------------------------------------------------------------------------*/
app.controller('MainCtrl', function ($scope, $rootScope, $http, $log, $timeout, $modal, $filter, uiGridConstants, ajaxService, usSpinnerService) {
  
 
  $scope.modal = {};
  $scope.modal.error = {};
  $scope.modal.book = {};
  
  
  $scope.showAddModal = false;
  $scope.showUpdateModal = false;
  
  $scope.startSpin = function(key) {
    usSpinnerService.spin(key);
  };
  
  $scope.stopSpin = function(key) {
    usSpinnerService.stop(key);
  };
 
  $scope.setTitleCase = function(input) {
    if (input != null ) {
      return input.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
    }
  };

  $scope.setLowerCase = function(input) {
    if (input != null ) {
      return input.replace(/\w\S*/g, function(txt){return txt.toLowerCase();});
    }
  };

  $scope.setUpperCase = function(input) {
		if (input != null ) {
			return input.replace(/\w\S*/g, function(txt){return txt.toUpperCase();});
		}
	};
  
  $scope.currentTimeMillis = function(ts) {
    var date = new Date().getTime();
    return date;
  };
  
  $scope.setDefaults = function(debugFlag, baseUrl) {
    categoryLoadUrl   = baseUrl + "/AngularUIGrid/rest/findbycategory?";
    titleLoadUrl    = baseUrl + "/AngularUIGrid/rest/findbytitle?";
    getAllBooksUrl  = baseUrl + "/AngularUIGrid/rest/books";
    getBookByISBNUrl  = baseUrl + "/AngularUIGrid/rest/getbookbyisbn?";
    createBookUrl   = baseUrl + "/AngularUIGrid/rest/book/add?";
    editBookUrl   = baseUrl + "/AngularUIGrid/rest/book/update?";
    deleteBookUrl   = baseUrl + "/AngularUIGrid/rest/book/delete?";
    allCategoriesUrl  = baseUrl + "/AngularUIGrid/rest/categories?";
    
    console.log("Setting Defaults");
    console.log("DebugFlag..........: " + debugFlag);
    console.log("categoryLoadUrl....: " + categoryLoadUrl);
    console.log("titleLoadUrl.......: " + titleLoadUrl);
    console.log("createBookUrl......: " + createBookUrl);
    console.log("editBookUrl........: " + editBookUrl);
    console.log("deleteBookUrl......: " + deleteBookUrl);
    console.log("getAllBooksUrl.....: " + getAllBooksUrl);
    console.log("allCategoriesUrl...: " + allCategoriesUrl);
    
    $scope.debugFlag = debugFlag;
    $scope.baseUrl = baseUrl;
    $scope.criteria = {};
    $scope.criteria.title = '';
    $scope.showCreateModal = false;
    $scope.showUpdateModal = false;
    $scope.showDeleteModal = false;
    $scope.showErrorModal = false;

  };

  $scope.$on('$viewContentLoaded', function() {
    console.log("viewContentLoaded event triggered...");
    loadUserDefaults();
  });
  
  $scope.showCreateBook= function () {
    $scope.modal = {};
    $scope.modal.book = {};
    $scope.showCreateModal = true;  
  }
  
  $scope.showErrorModalWindow = function(title, message) {
    $scope.modal.error = {};
    $scope.modal.error.title = title;
    $scope.modal.error.message = message;
    $scope.showErrorModal = true;
  }
  
  $scope.getAllBooks = function () {
    var url = getAllBooksUrl;
  
    $scope.startSpin('spinner-3');
    console.log("Inside getAllBooks " + url);

  function onSuccess(response) {
    console.log("+++++getAllBooks SUCCESS++++++");
    if ($scope.debugFlag == 'true') {
      console.log("Inside getAllBooks response..." + JSON.stringify(response.data));
    } else {
      console.log("Inside getAllBooks response...(XML response is being skipped, debug=false)");
    }
    if (response.data.success != false) { 
      $scope.gridOptions.data = response.data;
    } else {
      $scope.gridOptions.data = [];
    }
    $scope.stopSpin('spinner-3');
  };
    
  function onError(response) {
    console.log("-------getAllBooks FAILED-------");
    $scope.stopSpin('spinner-3');
    console.log("Inside getAllBooks error condition...");
  };
    
    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(url, 'GET', '').then(onSuccess, onError);
    
  };
  
  //---Cancel Modal Dialog Window---
  $scope.cancel = function () {
    console.log('Closing Modal Dialog Window...');
    $scope.showCreateModal = false;
    $scope.showUpdateModal = false;
    $scope.showDeleteModal = false;
  };
  
  //---Close Error Modal Window---
  $scope.closeError = function () {
    console.log('Closing Error Modal Window...');
    $scope.showErrorModal = false;
  };

  $scope.highlightFilteredHeader = function( row, rowRenderIndex, col, colRenderIndex ) {
    if( col.filters[0].term ) {
      return 'header_filtered';
    } else {
      return 'header_text';
    }
  };
  
  $scope.gridOptions = { 
    enableFiltering: true,
    enableCellEditOnFocus: false,
    enablePaginationControls: true,
    enableSorting: true,
    enableRowSelection: true,
    enableRowHeaderSelection: false,
    enableColumnResizing: true,
    paginationPageSizes: [10, 12, 15, 18],  //18
    paginationPageSize: 18,       //18
  };

  $scope.gridOptions.onRegisterApi = function(gridApi){
    //set gridApi on scope
    $scope.gridApi = gridApi;
    gridApi.edit.on.afterCellEdit($scope,function(rowEntity, colDef, newValue, oldValue){
      console.log('edited row uid:' + rowEntity.isbn13 + ' Column:' + colDef.name + ' newValue:' + newValue + ' oldValue:' + oldValue );
      $scope.$apply();
    });
  };
    
  $scope.gridOptions.multiSelect = false;

  //---Create Modal Dialog Window---
  $scope.createBook = function () {
    console.log('Inside createBook...');
    $scope.startSpin('spinner-4');
    
    
    createUrl = createBookUrl + "isbn13=" + $scope.modal.book.isbn13;
    createUrl += '&etc=' + new Date().getTime();

    console.log("createBook..: " + createUrl);
    
    $scope.modal.book.id = $scope.modal.book.isbn13;
    console.log("$scope.modal.book.id..........: " + $scope.modal.book.id);
    console.log("$scope.modal.book.isbn13..........: " + $scope.modal.book.isbn13);
    console.log("$scope.modal.book.title...........: " + $scope.modal.book.title);
    console.log("$scope.modal.book.author..........: " + $scope.modal.book.author);
    console.log("$scope.modal.book.publisher.......: " + $scope.modal.book.publisher);
    console.log("$scope.modal.book.isbn10..........: " + $scope.modal.book.isbn10);
    console.log("$scope.modal.book.description.....: " + $scope.modal.book.description);
    console.log("$scope.modal.book.dimensions......: " + $scope.modal.book.dimensions);
    console.log("$scope.modal.book.shippingWeight..: " + $scope.modal.book.shippingWeight);
    console.log("$scope.modal.book.language........: " + $scope.modal.book.language);
    console.log("$scope.modal.book.category........: " + $scope.modal.book.category);
    console.log("$scope.modal.book.active..........: " + $scope.modal.book.active);
    console.log("$scope.modal.book.quantity........: " + $scope.modal.book.quantity);
    console.log("$scope.modal.book.price...........: " + $scope.modal.book.price);

    if ($scope.debugFlag == 'true') {
      console.log("$scope.modal.book...: " + angular.toJson($scope.modal.book, true));
    }
    
    
     function onSuccess(response) {
      console.log("+++++createBook SUCCESS++++++");

      if ($scope.debugFlag == 'true') {
        console.log("Inside createBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside createBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.showCreateModal = false;       
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------createBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside createBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.postData(createUrl, 'POST', $scope.modal.book, '').then(onSuccess, onError);
    
  };
  
  //---Update Modal Dialog Window---
  $scope.updateBook = function (isbn13) {
    console.log('Inside updateBook...' + isbn13);
    $scope.startSpin('spinner-4');
    
    updateBookUrl = editBookUrl + "isbn13=" + isbn13;
    console.log("updateBook..: " + updateBookUrl);
    if ($scope.debugFlag == 'true') {
      console.log("$scope.modal.book...: " + angular.toJson($scope.modal.book, true));
    }
    
    function onSuccess(response) {
      console.log("+++++updateBook SUCCESS++++++");
      $scope.modal.row.entity.isbn13 = $scope.modal.book.isbn13;
      $scope.modal.row.entity.title = $scope.modal.book.title;
      $scope.modal.row.entity.author = $scope.modal.book.author;
      $scope.modal.row.entity.publisher = $scope.modal.book.publisher;
      $scope.modal.row.entity.isbn10 = $scope.modal.book.isbn10;
      $scope.modal.row.entity.dimensions = $scope.modal.book.dimensions;
      $scope.modal.row.entity.active = $scope.modal.book.active;
      $scope.modal.row.entity.price = $scope.modal.book.price;

      if ($scope.debugFlag == 'true') {
        console.log("Inside updateBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside updateBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      }
      $scope.stopSpin('spinner-4');
      $scope.showUpdateModal = false;
    };
  
    function onError(response) {
      console.log("-------updateBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside updateBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to PUT DATA----
    ajaxService.postData(updateBookUrl, 'PUT', $scope.modal.book, '').then(onSuccess, onError);
    
  };

  $scope.editBook = function(isbn13, row) {
    $scope.startSpin('spinner-4');

    console.log('Inside editBook: ' + isbn13);
    
    getBookURL = getBookByISBNUrl;
    getBookURL += "isbn13=" + isbn13;
    getBookURL += '&etc=' + new Date().getTime();
    
    console.log("getBookURL.........: " + getBookURL);
    
    //----- Get Book by ISBN13------
    
    function onSuccess(response) {
      console.log("+++++editBook SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside editBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside editBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.modal.row = row;
        $scope.modal.book.id = response.data.isbn13;
        $scope.modal.book.isbn13 = response.data.isbn13;
        $scope.modal.book.title = response.data.title;
        $scope.modal.book.author = response.data.author;
        $scope.modal.book.publisher = response.data.publisher;
        $scope.modal.book.isbn10 = response.data.isbn10;
        $scope.modal.book.dimensions = response.data.dimensions;
        $scope.modal.book.quantity = response.data.quantity;
        $scope.modal.book.description = response.data.description;
        $scope.modal.book.active = response.data.active;
        $scope.modal.book.shippingWeight = response.data.shippingWeight;
        $scope.modal.book.language = response.data.language;
        $scope.modal.book.price = response.data.price;
        $scope.showUpdateModal = true;
        if ($scope.debugFlag == 'true') {
          console.log(angular.toJson($scope.modal.book, true));
        }
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------editBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside editBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.getData(getBookURL, 'GET', '').then(onSuccess, onError);
        
  };
  
  $scope.deleteModal = function (row, isbn13, title) {
    console.log('Inside of deleteModal, isbn13 = ' + isbn13);
    $scope.modal = {};
    $scope.modal.book = {};
    
    $scope.modal.row = row;
    $scope.modal.book.isbn13 = isbn13;
    $scope.modal.book.title = title;
    $scope.showDeleteModal = true;
  };
  
  $scope.deleteBook = function (row, isbn13) {
    $scope.startSpin('spinner-4');

    console.log('Inside of deleteBook, isbn13 = ' + isbn13);
    
    delBookURL = deleteBookUrl;
    delBookURL += "isbn13=" + isbn13;
    delBookURL += '&etc=' + new Date().getTime();
    
    console.log("delBookURL.........: " + delBookURL);
    
    //----- Delete Book by ISBN13------
    function onSuccess(response) {
      console.log("+++++++deleteBook SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside deleteBook response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside deleteBook response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.deleteBookFromGrid(row, isbn13);
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------deleteBook FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside deleteBook error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to DELETE DATA----
    ajaxService.getData(delBookURL, 'DELETE', '').then(onSuccess, onError);
  };
  
  $scope.deleteBookFromGrid = function (row, isbn13) {
    var index = $scope.gridOptions.data.indexOf(row.entity);
    $scope.gridOptions.data.splice(index, 1);
    $scope.showDeleteModal = false;
  };  

  
  $scope.updateIsActive = function (isbn13, row, value) {
    var updateBookUrl; 
    
    value = !value;
    $scope.modal.book.active = value;
    
    updateBookUrl = editBookUrl + "isbn13=" +isbn13;
    console.log('Inside of updateIsActive ' + updateBookUrl + ', value = ' + value);
    $scope.startSpin('spinner-3');

    function onSuccess(response) {
      console.log("+++++updateIsActive SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside updateIsActive response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside updateIsActive response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        // Reset values in callback as there was a data problem on back-end...
        value = !value;
        row.entity.active = value; 
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        row.entity.active = value; 
      }
      $scope.stopSpin('spinner-3');
    };
  
    function onError(response) {
      console.log("-------updateIsActive FAILED-------");
      $scope.stopSpin('spinner-3');
      console.log("Inside updateIsActive error condition...");
    };

    if ($scope.debugFlag == 'true') {
      console.log(angular.toJson($scope.modal.book, true));
    }
    //----MAKE AJAX REQUEST CALL to POST DATA----
    ajaxService.postData(updateBookUrl, 'PUT', $scope.modal.book, '').then(onSuccess, onError);   
  };
  
  $scope.toggleIsActive = function (row, value) {
    var isbn13;
    
    $scope.startSpin('spinner-4');

    isbn13 = row.entity.isbn13;
    console.log('Inside toggleIsActive: ' + isbn13);
    
    getBookURL = getBookByISBNUrl;
    getBookURL += "isbn13=" + isbn13;
    getBookURL += '&etc=' + new Date().getTime();
    
    console.log("toggleIsActive.........: " + getBookURL);
    
    
    //----- Get Book by ISBN13------
    
    function onSuccess(response) {
      console.log("+++++toggleIsActive SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside toggleIsActive response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside toggleIsActive response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success == false) {
        $scope.showErrorModalWindow('Error!', response.data.message);
      } else {
        $scope.modal.row = row;
        $scope.modal.book.id = response.data.isbn13;
        $scope.modal.book.isbn13 = response.data.isbn13;
        $scope.modal.book.title = response.data.title;
        $scope.modal.book.author = response.data.author;
        $scope.modal.book.publisher = response.data.publisher;
        $scope.modal.book.isbn10 = response.data.isbn10;
        $scope.modal.book.dimensions = response.data.dimensions;
        $scope.modal.book.quantity = response.data.quantity;
        $scope.modal.book.description = response.data.description;
        $scope.modal.book.active = response.data.active;
        $scope.modal.book.shippingWeight = response.data.shippingWeight;
        $scope.modal.book.language = response.data.language;
        $scope.modal.book.price = response.data.price;
        if ($scope.debugFlag == 'true') {
          console.log(angular.toJson($scope.modal.book, true));
        }
        $scope.updateIsActive(isbn13, row, value);
      }
      $scope.stopSpin('spinner-4');
    };
  
    function onError(response) {
      console.log("-------toggleIsActive FAILED-------");
      $scope.stopSpin('spinner-4');
      console.log("Inside toggleIsActive error condition...");
      $scope.showErrorModalWindow('Error!', response.data.message);
    };

    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(getBookURL, 'GET', '').then(onSuccess, onError);
  }
  
  $scope.gridOptions.columnDefs = [
  { name: 'isbn13', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate:'<div class="editBook1"><a class="editBook" href="" ng-click="$event.stopPropagation(); grid.appScope.editBook(row.entity.isbn13, row);">'
      + '{{row.entity[col.field] | uppercase }}&nbsp; <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a></div>',
    cellClass: 'gridField', 
    displayName: 'ISBN13', 
    width: 140, 
    maxWidth: 160, 
    minWidth: 130, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'title', 
    headerCellClass: $scope.highlightFilteredHeader , 
    cellTemplate: '<div class="padded">{{row.entity.title}}</div>',
    cellClass: 'gridField', 
    displayName: 'Title', 
    width: 250, 
    maxWidth: 480, 
    minWidth: 220, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'author',
    headerCellClass: $scope.highlightFilteredHeader,
    cellTemplate: '<div class="centered">{{row.entity.author | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 160, 
    maxWidth: 200, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'publisher', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{row.entity.publisher | titlecase }}</div>',
    cellClass: 'gridField', 
    width: 90, 
    maxWidth: 200, 
    minWidth: 90, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'isbn10', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="centered">{{row.entity.isbn10}}</div>',
    cellClass: 'gridField', 
    displayName: 'ISBN10', 
    width: 80, 
    maxWidth: 140, 
    minWidth: 70, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'dimensions', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellTemplate: '<div class="padded">{{ row.entity.dimensions }}</div>',
    cellClass: 'gridField', 
    width: 130, 
    maxWidth: 220, 
    minWidth: 120, 
    enableHiding: false, 
    enableCellEdit: false },
  { name: 'active', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Is Active', 
    width: 70, 
    maxWidth: 120, 
    minWidth: 60, 
    enableHiding: false, 
    //type: 'boolean', 
    enableCellEdit: false, 
    cellTemplate: '<div class="isActive">'
      + '<a class="isActive" href="" ng-click="$event.stopPropagation(); row.entity.active=grid.appScope.toggleIsActive(row, row.entity.active);">' 
      + '<span ng-class="row.entity[col.field] ? \'checkIcon\' : \'xIcon\'">'
      + '<span ng-if="row.entity[col.field]">Yes</span>'
      + '<span ng-if="!row.entity[col.field]">No</span>'
      + '</span></a></div>'},
  { name: 'price', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Price', 
    width: 60, 
    maxWidth: 120, 
    minWidth: 50, 
    enableHiding: false, 
    enableCellEdit: false
  }, 
  { name: 'delete', 
    headerCellClass: $scope.highlightFilteredHeader, 
    cellClass: 'gridField', 
    displayName: 'Del', 
    width: 40, 
    maxWidth: 40, 
    minWidth: 40, 
    enableHiding: false, 
    enableCellEdit: false, 
    cellTemplate: '<div class="centered"><button id="cancel" type="button" class="btn btn-danger btn-xs" ng-click="$event.stopPropagation(); grid.appScope.deleteModal(row, row.entity.isbn13, row.entity.title)"><span class="glyphicon glyphicon-remove"></span></button></div>'    
  }
  ];

  $scope.gridOptions.data = [];
    

/* -----------------------------------------------------------------------
* CATEGORIES TREE  
--------------------------------------------------------------------------*/

  $scope.criteria = {};
  $scope.reverse = true;
  
  $scope.treeOptions = {
      nodeChildren: "item",
      dirSelectable: true
  };
  
  $scope.defaultData = [  { "text" : "All Books", "id" : "all", "item" : [] },
                       { "text" : "Books #1", "id" : "1", "item" : [
                         { "text" : "Books #2", "id" : "2", "item" : [] },
                         { "text" : "Books #3", "id" : "3", "item" : [] },
                         { "text" : "Books #4", "id" : "4", "item" : [] },
                         { "text" : "Books #5", "id" : "5", "item" : [] }
                       ]},
                       { "text" : "Books #6", "id" : "6", "item" : [] },
                       { "text" : "Books #7", "id" : "7", "item" : [] }
                     ];
  
  $scope.companyList = function() {
    return $scope.treedata;
  };

  $scope.node = function(num) {
    $scope.treedata = num;
    return $scope.treedata;
  };
  
  $scope.clearNode = function() {
    $scope.node = undefined;
  };
  
  $scope.getDefaultTreeData = function() {
    return $scope.defaultData;
  };
  
  $scope.processSelectedTreeNode = function (node) {
    var gridUrl, addl_params;
    
    
    if (node.id == '0') {
      gridUrl = titleLoadUrl;
      addl_params = 'title=' + $scope.criteria.title
        + '&etc=' + new Date().getTime();
    } else {
      gridUrl = categoryLoadUrl;
      addl_params = '&category=' + node.id 
        + '&title=' + $scope.criteria.title
        + '&etc=' + new Date().getTime();
    }
    $scope.startSpin('spinner-3');
    
    console.log("Inside processSelectedTreeNode Selected node is " + node.id);

    function onSuccess(response) {
      console.log("+++++processSelectedTreeNode SUCCESS++++++");
      if ($scope.debugFlag == 'true') {
        console.log("Inside processSelectedTreeNode response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside processSelectedTreeNode response...(XML response is being skipped, debug=false)");
      }
      if (response.data.success != false) { 
        console.log("response 'success'.."); 
        $scope.gridOptions.data = response.data;
      } else {
        console.log("response came back with 404!!!!");         
        $scope.gridOptions.data = [];
        $scope.showErrorModalWindow('Error!', response.data.message);
      }
      $scope.stopSpin('spinner-3');
    };
    
    function onError(response) {
      console.log("-------processSelectedTreeNode FAILED-------");
      $scope.stopSpin('spinner-3');
      console.log("Inside processSelectedTreeNode error condition...");
    };
    
    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(gridUrl, 'GET', addl_params).then(onSuccess, onError);      
  };

  $scope.treeUnset = function() {
    console.log("Inside of treeUnset...");
    $scope.clearNode();
  };
  
  $scope.resetForm = function () {
    $scope.criteria = {};
    $scope.criteria.title = '';
    $scope.gridOptions.data = [];
    $scope.$broadcast('setFocus');
  };

  $scope.greaterThan = function(id, val){
      return function(category){
        return category[id] > val;
      }
  };
  
  $scope.getAllCategories = function () {
    var url = allCategoriesUrl;

        console.log("Inside getAllCategories...");
      
    function onSuccess(response) {
      if ($scope.debugFlag == 'true') {
        console.log("Inside getAllCategories response..." + JSON.stringify(response.data));
      } else {
        console.log("Inside getAllCategories response...(XML response is being skipped, debug=false)");
      }
      $scope.treedata = response.data;
      $rootScope.treedata = response.data;
      //console.log("$rootScope.treedata: " + JSON.stringify($rootScope.treedata));
      $scope.stopSpin('spinner-2');
    };
    
    function onError(response) {
      $scope.stopSpin('spinner-2');
      console.log("Inside getAllCategories error condition...");
    };
    
    $scope.gridOptions.data = [];
    // Clear TreeView Selected Node
    $scope.treeUnset();
    $rootScope.treedata = [];
    $scope.treedata = [];
    
    $scope.startSpin('spinner-2');
    console.log("Inside getAllCategories..." + url);
    var addl_params ='etc='+new Date().getTime();
    
    console.log("$scope.criteria.title.............: " + $scope.criteria.title);
    
    //----MAKE AJAX REQUEST CALL to GET DATA----
    ajaxService.getData(url, 'GET', addl_params).then(onSuccess, onError);
      
  };
  
  $scope.processSelectedCriteria = function () {
        console.log("titleSearch----> " + JSON.stringify($scope.titleData));

    if ($scope.criteria.title != null && $scope.criteria.title.length >0) {
      $scope.getAllCategories();
    } else {
      // Length is not long enough
      $scope.showErrorModalWindow('Title Search Error!', 'The Title Search Criteria Field may NOT be empty.');
      console.log("Inside processSelectedCriteria, broadcasting setFocus event...");
      $scope.$broadcast('setFocus');
    }
  };
});

Cascading Style Sheet (CSS) (styles.css)

/* Need to default overrride this to affect grid footer (Total Items: xx) */
body {
  font-family: Arial !important;
  font-size: 12px !important;
  padding: 0px;
}

.grid {
  width: 99%;
  height: 40px;  // 40px; 
}

.dialogButtons {
  width: 98%;
  position: relative;
  text-align: right;
  height: 30px;
}

.dialogText {
  font-family: Arial;
  font-weight: normal;
  font-size: 14px;
  padding-top: 6px;
  color: #888;
}

.dialogTitle {
  font-family: Arial;
  font-weight: normal;
  font-size: 14px;
  padding-top: 6px;
  color: #0000F0;
}

.dialogErrorText {
  font-family: Arial;
  font-weight: bold;
  font-size: 12px;
  padding-top: 8px;
  color: #a94442;
}

.errorText {
  font-family: Arial;
  font-weight: bold;
  font-size: 14px;
  padding-top: 6px;
  color: #000;
}

/* Use this for Reports to Me Column  */
.redBox {
 background-image: url(../img/redBox.png);
 background-position: left center;
 background-repeat: no-repeat;
 padding-left: 12px; /* Or size of icon + spacing */
}

/* Use this for Reports to Me Column  */
.greenBox {
 background-image: url(../img/greenBox.png);
 background-position: left center;
 background-repeat: no-repeat;
 padding-left: 12px; /* Or size of icon + spacing */
 font-weight: bold;
}

/* Use this for Reports to Me Column  */
.xIcon {
 background-image: url(../img/x.png);
 background-position: left center;
 background-repeat: no-repeat;
 padding-left: 15px; /* Or size of icon + spacing */
 padding-top: 5px;
 font-weight: bold;
}

/* Use this for Reports to Me Column  */
.checkIcon {
 background-image: url(../img/check.png);
 background-position: left center;
 background-repeat: no-repeat;
 padding-left: 15px; /* Or size of icon + spacing */
 padding-top: 5px;
 font-weight: bold;
}

/* Use this for Reports to Me Column  */
.editBook1 {
  padding-top: 4px;
  text-align: center;
  color: #000;
}

.editBook {
  cursor: pointer;
  color: #000;
  font-family: Arial;
  font-size: 12px;
  padding: 3px 9px;
  text-decoration: underline;
}

.padded {
  padding-left: 2px;
  padding-right: 2px;
}

.centered {
  text-align: center;
}

/* Use this for Reports to Me Column  */
.isActive {
  padding-top: 4px;
  text-align: center;
  color: #000;
}

.isActive:link {
  color: #000;
  //font-weight: bold;
  text-decoration: none;
}

.sidebar {
  padding:  10px 0px 0px 10px;
  position: relative;
  overflow: auto;
  height: 750px;
}

.panel_north, .panel_west, .panel_east, .panel_south, .panel_center  {
    position: fixed;
    left:5; top:5; right:5; bottom:5;
    margin: 0px;
    padding:20px;
}

.panel_north {
  /*bottom:80%;*/
  height: 155px;
  width: 99%;
  float: top;
  padding: 0px; 
  background: white;
  
  /* border-style: solid solid none solid;
  border-width: 1px;
  border-color: #999; */
}

.panel_west {
  top: 155px;
  width: 230px;
  /* right: 80%; */
  height: 100%;
  padding: 0px;
  margin-top: 0px;
  margin-right: 0px;
  background: white; 
   
  border-style: solid solid solid solid;
  border-width: 1px;
  border-color: #999;
}

.panel_east {
  top: 155px;
  height: 100%;
  left: 90%;
  right: 0px;
  padding: 10px;
  margin-top: 0px;
  margin-right: 0px;
  background: yellow; 
  
  border-style: solid solid solid solid;
  border-width: 1px;
  border-color: #999;
  overflow: auto;
}

.panel_south {
  bottom: 20%;
  padding: 10px; 
  background: white;  
}

.panel_center {
  top: 155px;
  left: 235px;
  width: 90%;
  height: 100%;
  padding: 0px;
  background: white;
  
  margin-top: 0px;
  margin-left: 5px;
  margin-right: 0px;
  
  border-style: solid solid solid solid;
  border-width: 1px;
  border-color: #999;
  
  overflow: auto;
}

.inset {
  width: 1050px;        /* Tuned to 1280x1024 pixel*/
  overflow: auto;
  overflow-y: hidden;
  -ms-overflow-y: hidden; 
}

.spacer_10 {
  width: 10px;
  height: 10px;
  background: white;
}

.header { 
  background: #419641;
  /* background: #2589CE; */
  color: #FFF;
  font-family: Tahoma, sans-serif;
  font-size: 13px;
  font-weight: bold;
  text-align: left;
  /* T R B L */
  padding: 7px 0px 8px 10px;
  position: relative;
  overflow: hidden;
}

.header_text {
  background: #62C462;
  background: -ms-linear-gradient(top, #62C462 0%, #357A35 100%);
  background: -moz-linear-gradient(top, #62C462 0%, #357A35 100%);
  background: -o-linear-gradient(top, #62C462 0%, #357A35 100%);
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #62C462), color-stop(100, #357A35));
  background: -webkit-linear-gradient(top, #62C462 0%, #357A35 100%);
  background: linear-gradient(to bottom, #62C462 0%, #357A35 100%);
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#488ecf', endColorstr='#062a4f', GradientType=0 );
  font-family: Arial;
  font-weight: bold;
  font-size: 12px;
  color: #FFF;
  height: 40px !important;
}

.header_filtered {
  background: rgba(247,232,62,1);
  background: -moz-linear-gradient(top, rgba(247,232,62,1) 0%, rgba(204,149,38,1) 100%);
  background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(247,232,62,1)), color-stop(100%, rgba(204,149,38,1)));
  background: -webkit-linear-gradient(top, rgba(247,232,62,1) 0%, rgba(204,149,38,1) 100%);
  background: -o-linear-gradient(top, rgba(247,232,62,1) 0%, rgba(204,149,38,1) 100%);
  background: -ms-linear-gradient(top, rgba(247,232,62,1) 0%, rgba(204,149,38,1) 100%);
  background: linear-gradient(to bottom, rgba(247,232,62,1) 0%, rgba(204,149,38,1) 100%);
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f7e83e', endColorstr='#cc9526', GradientType=0 );

  padding: 0px 0px 0px 0px;
  font-family: Arial;
  font-weight: bold;
  font-size: 12px;
  color: #0d1b75;
  height: 40px !important;
}

.gridHeader {
  font-family: Arial;
  font-weight: bold;
  height: 20px !important;
  font-size: 12px;
  color: #FFF;
  /* border:1px solid #009; */ 
  border-collapse:collapse;

  background: rgba(72,142,207,1);
  background: -moz-linear-gradient(top, rgba(72,142,207,1) 0%, rgba(6,42,79,1) 100%);
  background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(72,142,207,1)), color-stop(100%, rgba(6,42,79,1)));
  background: -webkit-linear-gradient(top, rgba(72,142,207,1) 0%, rgba(6,42,79,1) 100%);
  background: -o-linear-gradient(top, rgba(72,142,207,1) 0%, rgba(6,42,79,1) 100%);
  background: -ms-linear-gradient(top, rgba(72,142,207,1) 0%, rgba(6,42,79,1) 100%);
  background: linear-gradient(to bottom, rgba(72,142,207,1) 0%, rgba(6,42,79,1) 100%);
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#488ecf', endColorstr='#062a4f', GradientType=0 );
}

.gridField {
  font-family: Arial;
  font-size: 12px;
  color: #444;
}

.main_text {
  font-family: Arial Black;
  font-weight: bold;
  font-size: 25pt;
  color: #000
}

.date_text {
  font-family: Arial;
  font-size: 8pt;
  font-weight: bold;  
  color: #000
}

.div_default {
  /* T R B L */ 
  padding: 10px 0px 0px 10px;
  text-align: left;
  background: #FFF;
}

.label_text {
  font-family: Arial;
  font-size: 12px;
  padding: 0px 0px 0px 0px;
  color: #000;
}

.subheading_text {
  font-family: Verdana;
  font-size: 14px;
  font-weight: bold;  
  padding: 0px 0px 0px 0px;
  color: #000;
}

.toptable {
  padding: 0px 0px 0px 0px !important;
}

.button_default {
  font-family: Arial;
  font-size: 12px;
  color: #FFF;
  padding: 5px 20px 5px 20px;
  border: none;
  background: #2589CE;
}

.button_default:hover {
  font-family: Arial;
  font-size: 12px;
  color: #FFF;
  padding: 5px 20px 5px 20px;
  border: none;
  background: #259AEE;
}

.button_default:active {
  font-family: Arial;
  font-size: 12px;
  color: #FFF;
  border: none;
  background: #256585;
}

JSON Books Documents

I am including JSON documents in the event that you may want to pre-populate your MongoDB collection.

{
    "_id" : "978-0544272996",
    "title" : "What If?: Serious Scientific Answers to Absurd Hypothetical Questions",
    "author" : "Randall Munroe",
    "publisher" : "Houghton Mifflin Harcourt; First Edition edition (September 2, 2014)",
    "isbn10" : "0544272994",
    "isbn13" : "978-0544272996",
    "language" : "english",
    "category" : "16000",
    "dimensions" : "7 x 1.2 x 9 inches",
    "shippingWeight" : "1.8 pounds",
    "description" : "Millions of people visit xkcd.com each week to read Randall Munroe’s iconic webcomic. His stick-figure drawings about science, technology, language, and love have a large and passionate following.  Fans of xkcd ask Munroe a lot of strange questions. What if you tried to hit a baseball pitched at 90 percent the speed of light? How fast can you hit a speed bump while driving and live? If there was a robot apocalypse, how long would humanity last?  In pursuit of answers, Munroe runs computer simulations, pores over stacks of declassified military research memos, solves differential equations, and consults with nuclear reactor operators. His responses are masterpieces of clarity and hilarity, complemented by signature xkcd comics. They often predict the complete annihilation of humankind, or at least a really big explosion.",
    "price" : 12.57,
    "quantity" : 32,
    "active" : true
}

{
    "_id" : "978-0805098105",
    "title" : "Missing Microbes: How the Overuse of Antibiotics Is Fueling Our Modern Plagues",
    "author" : "Martin J. Blaser",
    "publisher" : "Henry Holt and Co.; 1 edition (April 8, 2014)",
    "isbn10" : "0805098100",
    "isbn13" : "978-0805098105",
    "language" : "italian",
    "category" : "16000",
    "dimensions" : "6.3 x 1 x 9.4 inches",
    "shippingWeight" : "1 pounds",
    "description" : "A critically important and startling look at the harmful effects of overusing antibiotics, from the field's leading expert Tracing one scientist's journey toward understanding the crucial importance of the microbiome, this revolutionary book will take readers to the forefront of trail-blazing research while revealing the damage that overuse of antibiotics is doing to our health: contributing to the rise of obesity, asthma, diabetes, and certain forms of cancer. In Missing Microbes, Dr. Martin Blaser invites us into the wilds of the human microbiome where for hundreds of thousands of years bacterial and human cells have existed in a peaceful symbiosis that is responsible for the health and equilibrium of our body. Now, this invisible eden is being irrevocably damaged by some of our most revered medical advances--antibiotics--threatening the extinction of our irreplaceable microbes with terrible health consequences. Taking us into both the lab and deep into the fields where these troubling effects can be witnessed firsthand, Blaser not only provides cutting edge evidence for the adverse effects of antibiotics, he tells us what we can do to avoid even more catastrophic health problems in the future.",
    "price" : 21.03,
    "quantity" : 3,
    "active" : false
}

{
    "_id" : "978-1476708690",
    "title" : "The Innovators: How a Group of Hackers, Geniuses, and Geeks Created the Digital Revolution",
    "author" : "Walter Isaacson",
    "publisher" : "Simon & Schuster; 1St Edition edition (October 7, 2014)",
    "isbn10" : "147670869X",
    "isbn13" : "978-1476708690",
    "language" : "english",
    "category" : "16000",
    "dimensions" : "6.1 x 1.6 x 9.2 inches",
    "shippingWeight" : "1.8 pounds",
    "description" : "Following his blockbuster biography of Steve Jobs, The Innovators is Walter Isaacson’s revealing story of the people who created the computer and the Internet. It is destined to be the standard history of the digital revolution and an indispensable guide to how innovation really happens.  What were the talents that allowed certain inventors and entrepreneurs to turn their visionary ideas into disruptive realities? What led to their creative leaps? Why did some succeed and others fail?  In his masterly saga, Isaacson begins with Ada Lovelace, Lord Byron’s daughter, who pioneered computer programming in the 1840s. He explores the fascinating personalities that created our current digital revolution, such as Vannevar Bush, Alan Turing, John von Neumann, J.C.R. Licklider, Doug Engelbart, Robert Noyce, Bill Gates, Steve Wozniak, Steve Jobs, Tim Berners-Lee, and Larry Page",
    "price" : 23.84,
    "quantity" : 7,
    "active" : true
}

{
    "_id" : "978-1476770383",
    "title" : "Revival: A Novel",
    "author" : "Stephen King",
    "publisher" : "Scribner; 1st edition (November 11, 2014)",
    "isbn10" : "1476770387",
    "isbn13" : "978-1476770383",
    "language" : "english",
    "category" : "13000",
    "dimensions" : "6.1 x 1.4 x 9.2 inches",
    "shippingWeight" : "1.6 pounds",
    "description" : "A dark and electrifying novel about addiction, fanaticism, and what might exist on the other side of life.  In a small New England town, over half a century ago, a shadow falls over a small boy playing with his toy soldiers. Jamie Morton looks up to see a striking man, the new minister. Charles Jacobs, along with his beautiful wife, will transform the local church. The men and boys are all a bit in love with Mrs. Jacobs; the women and girls feel the same about Reverend Jacobs—including Jamie’s mother and beloved sister, Claire. With Jamie, the Reverend shares a deeper bond based on a secret obsession. When tragedy strikes the Jacobs family, this charismatic preacher curses God, mocks all religious belief, and is banished from the shocked town.  Jamie has demons of his own. Wed to his guitar from the age of thirteen, he plays in bands across the country, living the nomadic lifestyle of bar-band rock and roll while fleeing from his family’s horrific loss. In his mid-thirties—addicted to heroin, stranded, desperate—Jamie meets Charles Jacobs again, with profound consequences for both men. Their bond becomes a pact beyond even the Devil’s devising, and Jamie discovers that revival has many meanings.",
    "price" : 14.71,
    "quantity" : 12,
    "active" : true
}

JSON Category Documents

{ _id: 0, text: "All Books", items: [] }
{ _id: 1000, text: "Arts & Photography", items: [] }
{ _id: 2000, text: "Audiobooks", items: [] }
{ _id: 3000, text: "Biographies & Memoirs", items: [] }
{ _id: 4000, text: "Business & Investing", items: [] }
{ _id: 5000, text: "Children's Books", items: [] }
{ _id: 6000, text: "Comics & Graphic Novels", items: [] }
{ _id: 7000, text: "Cookbooks & Food Writing", items: [] }
{ _id: 8000, text: "Crafts, Home & Garden", items: [] }
{ _id: 9000, text: "Gift Picks", items: ["90100", "90200", "90300", "90400", "90500", "90600", "90700", "90800", "90900", "91000"] }
{ _id: 10000, text: "History", items: [] }
{ _id: 11000, text: "Humor & Entertainment", items: [] }
{ _id: 12000, text: "Literature & Fiction", items: [] }
{ _id: 13000, text: "Mystery, Thriller & Suspense", items: [] }
{ _id: 14000, text: "Nonfiction", items: [] }
{ _id: 15000, text: "Romance", items: [] }
{ _id: 16000, text: "Science", items: [] }
{ _id: 17000, text: "Science Fiction & Fantasy", items: [] }
{ _id: 18000, text: "Sports & Outdoors", items: [] }
{ _id: 19000, text: "Teen & Young Adult", items: [] }
{ _id: 90100, text: "Little Bookworms", items: [] }
{ _id: 90200, text: "Fun & Quirky", items: [] }
{ _id: 90300, text: "Eat, Drink, Read", items: [] }
{ _id: 90400, text: "For the Young at Heart", items: [] }
{ _id: 90500, text: "Fantastic Fiction", items: [] }
{ _id: 90600, text: "Nothing But the Truth", items: [] }
{ _id: 90700, text: "Eye Candy", items: [] }
{ _id: 90800, text: "Cops & Crooks", items: [] }
{ _id: 90900, text: "Secrets of Success", items: [] }
{ _id: 91000, text: "Design, Construct, Create", items: [] }

Let’s Test out the Spring MVC REST API calls

For this server-side test, I will be using I’m Only Resting application from Swen Sen Software.

Let’s View the AngularJS/Bootstrap GUI Screens

As you can see from the following screenshots, the UI is quite appealing in its design.

Download the Code

That’s It!

I hope you enjoyed this tutorial. It was certainly a lot of fun putting it together and testing it out. Please continue to share the love and like us so that we can continue bringing you quality tutorials. Happy Coding!!!

angularjs user interface

Please Share Us on Social Media

Facebooktwittergoogle_plusredditpinterestlinkedinmail

Leave a Reply

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