This recipe explains the installation and the configuration of AngularJS to manage a single-page web application.
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.
v2.x.x
branch, the index.jsp
file has been added an Angular directive to the HTML tag:<HTML ng-app="cloudStreetMarketApp">
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.
cloudstreetmarket-webapp/src/main/webapp/js
directory.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(); });
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(); });
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.
AngularJS is automatically initialized when the DOM is loaded.
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.
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.
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; }
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.
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}}
.
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.
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>
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>
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.
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:
cloudStreetMarketApp.controller('homeCommunityActivityController', ['$scope', 'communityFactory', function ($scope, communityFactory){ communityFactory.pull(); $scope.communityActivities = communityFactory.fetchData(); }]);
$inject
property annotation:var homeCommunityActivityController = function ($scope, communityFactory){ communityFactory.pull(); $scope.communityActivities = communityFactory.fetchData(); } homeCommunityActivityController.$inject = ['$scope', 'communityFactory']; cloudStreetMarketApp.controller('homeCommunityActivityController', homeCommunityActivityController);
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.
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: