Designing a client-side MVC pattern with AngularJS

This recipe explains the installation and the configuration of AngularJS to manage a single-page web application.

Getting ready

In this recipe, we explain how we got rid of the rendering logic introduced previously in the JSPs to build the DOM. We will now rely on AngularJS for this job.

Even if we don't have yet a REST API that our frontend could query, we will temporarily make the JSP build the needed JavaScript objects as if they were provided by the API.

AngularJS is an open source Web application Framework. It provides support for building single-page applications that can directly accommodate microservice architecture requirements. The first version of AngularJS was released in 2009. It is now maintained by Google and an open source community.

AngularJS is a whole topic in itself. As a Framework, it's deep and wide at the same time. Trying to present it as a whole would take us beyond the scope of this book and wouldn't really suit our approach.

For this reason, we are going to highlight details, features, and characteristics of the Framework that we can use to our advantage for the application.

How to do it...

Setting up the DOM and creating modules

  1. Still from the previously checked-out v2.x.x branch, the index.jsp file has been added an Angular directive to the HTML tag:
    <HTML ng-app="cloudStreetMarketApp">
  2. The AngularJS JavaScript library (angular.min.js from https://angularjs.org) has been placed in the cloudstreetmarket-webapp/src/main/webapp/js directory.

    The index.jsp file has been added a wrapper landingGraphContainerAndTools div around landingGraphContainer, a select box and an ng-controller="homeFinancialGraphController":

    <div id='landingGraphContainer' ng-controller="homeFinancialGraphController">
        <select class="form-control centeredElementBox">
          <option value="${dailyMarketActivity.marketId}"> 
          ${dailyMarketActivity.marketShortName}</option>
        </select> 
      </div>

    The whole tableMarketPrices div has been reshaped in the following way:

    <div id='tableMarketPrices'>
        <script>
          var dailyMarketsActivity = [];
          var market;
        </script>
        <c:forEach var="market" items="${dailyMarketsActivity}">
        <script>
          market = {};
          market.marketShortName = '${market.marketShortName}';
          market.latestValue = (${market.latestValue}).toFixed(2);
          market.latestChange = 	(${market.latestChange}*100).toFixed(2);
          dailyMarketsActivity.push(market);
        </script>
        </c:forEach>
    <div>
    <table class="table table-hover table-condensed table-bordered table-striped" data-ng-controller='homeFinancialTableController'>
        <thead>
          <tr>
            <th>Index</th>
            <th>Value</th>
            <th>Change</th>
          </tr>
        </thead>
        <tbody>
            <tr data-ng-repeat="value in financialMarkets">
            <td>{{value.marketShortName}}</td>
            <td style="text-	align:right">{{value.latestValue}}</td>
            <td class='{{value.style}}' style="text-align:right">
            <strong>{{value.latestChange}}%</strong>
            </td>
          </tr>
        </tbody>
        </table>
        </div>
    </div>	

    Then, the <div id="divRss3"> div has received significant refactoring:

    <div id="divRss3">
      <ul class="feedEkList" data-ng-controller='homeCommunityActivityController'>
        <script>
          var userActivities = [];
          var userActivity;
         </script>
          <c:forEach var="activity" items="${recentUserActivity}">
          <script>
            userActivity = {};
            userActivity.userAction = '${activity.userAction}';
             userActivity.urlProfilePicture = '${activity.urlProfilePicture}';
               userActivity.userName = '${activity.userName}';
               userActivity.urlProfilePicture = 	'${activity.urlProfilePicture}';
            userActivity.date = '<fmt:formatDate ="${activity.date}" pattern="dd/MM/yyyy hh:mm aaa"/>';
            userActivity.userActionPresentTense = '${activity.userAction.presentTense}';
            userActivity.amount = ${activity.amount};
            userActivity.valueShortId = '${activity.valueShortId}';
            userActivity.price = (${activity.price}).toFixed(2);
            userActivities.push(userActivity);
          </script>
           </c:forEach>
        <li data-ng-repeat="value in communityActivities">
        <div class="itemTitle">
        <div class="listUserIco {{value.defaultProfileImage}}">
          <img ng-if="value.urlProfilePicture" src='{{value.urlProfilePicture}}'>
        </div>
        <span class="ico-white {{value.iconDirection}} listActionIco"></span>
          <a href="#">{{value.userName}}</a> 
          {{value.userActionPresentTense}} {{value.amount}} 
          <a href="#">{{value.valueShortId}}</a> at {{value.price}}
          <p class="itemDate">{{value.date}}</p>
          </div>
        </li>
      </ul>
    </div>

    The graph generation section has disappeared, and it is now replaced with:

    <script>
      var cloudStreetMarketApp = 
        angular.module('cloudStreetMarketApp', []);
      var tmpYmax = <c:out 
        value="${dailyMarketActivity.maxValue}"/>;
      var tmpYmin = <c:out 	
        value="${dailyMarketActivity.minValue}"/>;
    </script>

    This graph generation has been externalized in one of the three custom JavaScript files, included with the declarations:

    <script src="js/angular.min.js"></script>
    
    <script src="js/home_financial_graph.js"></script>
    <script src="js/home_financial_table.js"></script>
    <script src="js/home_community_activity.js"></script>

We are going to see those three custom JavaScript files next.

Defining the module's components

  1. As introduced, three custom JavaScript files are located in the cloudstreetmarket-webapp/src/main/webapp/js directory.
  2. The first one, home_financial_graph.js, relates to the graph. It creates a factory whose ultimate role is to pull and provide data:
    cloudStreetMarketApp.factory("financialDataFactory", function () {
        return {
            getData: function (market) {
              return financial_data;
            },
            getMax: function (market) {
              return tmpYmax;
            },
            getMin: function (market) {
              return tmpYmin;
            }
        }
    });

    This same file also creates a controller:

    cloudStreetMarketApp.controller('homeFinancialGraphController', function ($scope, financialDataFactory){
      readSelectValue();
      drawGraph();
      $('.form-control').on('change', function (elem) {
        $('#landingGraphContainer').html('');
        readSelectValue()
        drawGraph();
      });
      function readSelectValue(){
      $scope.currentMarket = $('.form-control').val();
      }
      function drawGraph(){
        Morris.Line({
          element: 'landingGraphContainer',
            hideHover: 'auto',
            data:financialDataFactory.getData($scope.currentMarket),
            ymax: financialDataFactory.getMax($scope.currentMarket),
            ymin: financialDataFactory.getMin($scope.currentMarket),
            pointSize: 3,
            hideHover:'always',
            xkey: 'period', xLabels: 'time',
            ykeys: ['index'], postUnits: '',
            parseTime: false, labels: ['Index'],
            resize: true, smooth: false,
            lineColors: ['#A52A2A']
          });
      }
    });

    The second file: home_financial_table.js relates to the markets overview table. Just like home_financial_graph.js, it creates a factory:

    cloudStreetMarketApp.factory("financialMarketsFactory", function () {
      var data=[];
        return {
            fetchData: function () {
              return data;
            },
            pull: function () {
            $.each( dailyMarketsActivity, function(index, el ) {
              if(el.latestChange >=0){
                dailyMarketsActivity[index].style='text-success';
              }
              else{
                dailyMarketsActivity[index].style='text-error';
              }
            });
            data = dailyMarketsActivity;
            }
        }
    });

    The home_financial_table.js file also have its own controller:

    cloudStreetMarketApp.controller('homeFinancialTableController', function ($scope, financialMarketsFactory){
       financialMarketsFactory.pull();
       $scope.financialMarkets = financialMarketsFactory.fetchData();
    });
  3. The third and last file, home_community_activity.js relates to the community activity table. It defines a factory:
    cloudStreetMarketApp.factory("communityFactory", function () {
      var data=[];
        return {
            fetchData: function () {
              return data;
            },
            pull: function () {
            
            $.each( userActivities, function(index, el ) {
            if(el.userAction =='BUY'){
              userActivities[index].iconDirection='ico-up-arrow actionBuy';
              }
              else{
              userActivities[index].iconDirection='ico-down-arrow actionSell';
            }
            userActivities[index].defaultProfileImage='';
            if(!el.urlProfilePicture){
              userActivities[index].defaultProfileImage='ico-	user';
            }
            userActivities[index].price='$'+el.price;
            });
            data = userActivities;
            }
        }
    });

    And its controller:

    cloudStreetMarketApp.controller('homeCommunityActivityController', function ($scope, communityFactory){
       communityFactory.pull();
       $scope.communityActivities = communityFactory.fetchData();
    });

How it works...

To understand better how our AngularJS deployment works, we will see how AngularJS is started and how our Angular module (app) is started. Then, we will discover the AngularJS Controllers and factories and finally the implemented Angular directives.

One app per HTML document

AngularJS is automatically initialized when the DOM is loaded.

Note

The Document Object Model (DOM) is the cross-platform convention for interacting with HTML, XHTML objects. When the browser loads a web page, it creates a Document Object Model of this page.

AngularJS looks up the DOM for an ng-app declaration in order to bind a module against a DOM element and start (autobootstrap) this module. Only one application (or module) can be autobootstrapped per HTML document.

We can still define more than one application per document and bootstrap them manually, though, if required. But the AngularJS community drives us towards binding an app to an HTML or BODY tag.

Module autobootstrap

Our application is autobootstrapped because it's referenced in the HTML tag:

<HTML ng-app="cloudStreetMarketApp">

Also, because the module has been created (directly in a <script> element of the HTML document):

var cloudStreetMarketApp= angular.module('cloudStreetMarketApp', []);

Tip

Note the empty array in the module creation; it allows the injection of dependencies into the module. We will detail the AngularJS dependency injection shortly.

Manual module bootstrap

As introduced before, we can bootstrap an app manually, especially if we want to control the initialization flow, or if we have more than one app per document. The code is as follows:

angular.element(document).ready(function() {
      angular.bootstrap(document, ['myApp']);
});

AngularJS Controllers

AngularJS controllers are the central piece of the Framework. They monitor all the data changes occurring on the frontend. A controller is bound to a DOM element and corresponds to a functional and visual area of the screen.

At the moment, we have defined three controllers for the market graph, the markets list, and for the community activity feed. We will also need controllers for the menus and for the footer elements.

AngularJS Controllers

The DOM binding is operated with the directive's ng-controller:

<div ng-controller="homeFinancialGraphController">
  <table data-ng-controller='homeFinancialTableController'>
  <ul data-ng-controller='homeCommunityActivityController'>

Each controller has a scope and this scope is being passed as a function-argument on the controller's declaration. We can read and alter it as an object:

cloudStreetMarketApp.controller('homeCommunityActivityController', function ($scope, communityFactory){
  ...
  $scope.communityActivities = communityFactory.fetchData();
  $scope.example = 123;
}

Bidirectional DOM-scope binding

The scope is synchronized with the DOM area the controller is bound to. AngularJS manages a bidirectional data-binding between the DOM and the controller's scope. This is probably the most important AngularJS feature to understand.

Tip

The AngularJS model is the controller's scope object. Unlike Backbone.js, for example, there is not really a view layer in Angular since the model is directly reflected in the DOM.

The content of a scope variable can be rendered in the DOM using the {{…}} notation. For example, the $scope.example variable can be fetched in the DOM with {{example}}.

AngularJS directives

The directives are also a famous feature of AngularJS. They provide the ability of attaching directly to the DOM some. We can create our own directives or use built-in ones.

We will try to visit as many directives as we can along this book. For the moment, we have used the following.

ng-repeat

In order to iterate the communityActivities and financialMarkets collections, we define a local variable name as part of the loop and each item is accessed individually with the {{…}} notation. The code is as follows:

<li data-ng-repeat="value in communityActivities">
  <div class="itemTitle">
    <div class="listUserIco {{value.defaultProfileImage}}">
     <img ng-if="value.urlProfilePicture" src='{{value.urlProfilePicture}}'>
    </div>
    ...
  </div>
</li>

ng-if

This directive allows removing, creating, or recreating an entire DOM element or DOM hierarchy depending on a condition.

In the next example, the {{value.defaultProfileImage}} variable only renders the CSS class ".ico-user" when the user doesn't have a custom profile image (in order to display a default generic profile picture).

When the user has a profile picture, the value.urlProfilePicture variable is therefore populated, the ng-if condition is satisfied, and the <img> element is created in the DOM. The code is as follows:

<div class="listUserIco {{value.defaultProfileImage}}">
  <img ng-if="value.urlProfilePicture" src='{{value.urlProfilePicture}}'>
</div>

AngularJS factories

Factories are used to obtain new object instances. We have used factories as data generator. We will also use them as services coordinator and intermediate layer between the services and Controller. The Services will pull the data from the server APIs. The code is as follows:

cloudStreetMarketApp.factory("communityFactory", function () {
  var data=[];
    return {
        fetchData: function () {
        return data;
        },
        pull: function () {
        $.each( userActivities, function(index, el ) {
          if(el.userAction =='BUY'){
            userActivities[index].iconDirection='ico-up-arrow     actionBuy';
          }
          else{
          userActivities[index].iconDirection='ico-down-arrow actionSell';
          }
          userActivities[index].defaultProfileImage='';
          if(!el.urlProfilePicture){
          userActivities[index].defaultProfileImage='ico-user';
          }
          userActivities[index].price='$'+el.price;
        });
        data = userActivities;
        }
    }
});

In this factory, we define two functions: pull() and fetchData() that populate and retrieve the data:

cloudStreetMarketApp.controller('homeCommunityActivityController', function ($scope, communityFactory){
   communityFactory.pull();
   $scope.communityActivities = communityFactory.fetchData();
});

Once the controller is loaded, it will pull() and fetchData() into the $scope.communityActivities. These operations are in this case executed only once.

Tip

Our factories are injected as dependencies into our controller declarations:

cloudStreetMarketApp.controller('homeCommunityActivityController', function ($scope, communityFactory)

Dependency injection

In our factories, controllers, and module definitions, we use AngularJS Dependency Injection to handle the components' lifecycle and their dependencies.

AngularJS uses an injector to perform the configured injections. There are three ways of annotating dependencies to make them eligible for injection:

  • Using the inline array annotation:
    cloudStreetMarketApp.controller('homeCommunityActivityController', ['$scope', 'communityFactory', function ($scope, 
    communityFactory){
       communityFactory.pull();
       $scope.communityActivities = communityFactory.fetchData();
    }]);
  • Using the $inject property annotation:
    var homeCommunityActivityController = function ($scope, 
    communityFactory){
       communityFactory.pull();
       $scope.communityActivities = communityFactory.fetchData();
    }
    homeCommunityActivityController.$inject = ['$scope', 'communityFactory'];
    cloudStreetMarketApp.controller('homeCommunityActivityController', homeCommunityActivityController);
  • Using the implicit annotation mode from the function parameter names:
    cloudStreetMarketApp.controller('homeCommunityActivityController', function ($scope, communityFactory){
        communityFactory.pull();
        $scope.communityActivities = communityFactory.fetchData();
    });

While we have been using mostly the implicit annotation style and the inline array annotation style, we have to highlight the fact that the implicit annotation dependency injection will not work using JavaScript minification.

There's more...

As you may imagine, this has been a quick introduction of AngularJS. We will discover more of it in-situ when we have a REST API and more features in our application.

AngularJS is becoming very popular and an active community is supporting it. Its core idea and implementation, based on an explicit DOM, provide a radical and simplified way of getting in touch with an application.

The documentation is very detailed: https://docs.angularjs.org.

There are loads of tutorials and videos on the web:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset