Reusable AngularJS Component: Warning Stripe

Credit: CodeProject.com, Used under: fair use/non-profit/educational purpose.

Originally published on CodeProject.com. Published on Aug 3rd, 2020.

Introduction

One of the cool aspects of AngularJS are the directives. Directives can be created as customized components that can be placed on the html page. According to AngularJS documentation, directives are DOM element markers. When AngularJS applications runs, it compiles these marker, create additional DOM elements as children to the marker, and attach dynamic behaviors to it. This is a basic introduction of what a directive is.

Let's look at an example:

<select class="form-control" ng-model="vm.selectedValue" ng-options="item as item.text for item of vm.optionValues">
</select>

This is a simple drop down list. Inside, there are two directives, one is ng-model, it can also be referred as ngModel. This directive is the most common one, it binds a view model property to the UI element. The other ng-options or ngOptions is another directive. It iterates through the data model, in this case, a list of items. And display the items in the drop down control. These two are attributes based directives. For this tutorial I will create a simple directive to demo how a typical directive can be created. It will be a element or attribute based directive.

Overview of the Sample Project

The sample project has just one page, with a user registration form. When user enters data on this page and something is missing, a warning stripe will be displayed on the top. To dismiss this warning stripe, user can click anyway in it, and it will disappear. When all required information has been entered, then user click on the button "Submit", the warning display will be replaced with a success message.

Here is a screenshot of the page with no user data entered:

Here is a screenshot of the page with error displayed:

When the user data is successfully submitted, here is a screenshot of the page with success message displayed:

The actual component that I want to share in this tutorial is this warning stripe. It is one of these little HTML pieces that can be place at anywhere in an web application. The code for such component can be duplicated everywhere, and most of the time, change one of them does not have to be replicated at other places. But I had code duplication. Whenever there is an opportunity to create something reusable, I would try to create it. This tutorial will show how this little warning stripe is created, and how it can be used.

The sample project is Spring Boot based. The Java side of the project only to host static contents - the page that display the user registration, as well as the javascript files, and other page files. I won't show any code from the Java side. If you are interested, please check them out when you have time.

Next, I will start discussing the design of the directive, which is all JavaScript and HTML.

The Design of the Warning Stripe Directive

The reason I call this directive a warning stripe is that in most of the cases, this directive will display errors or warnings. When it display a warning or an error, the background color will be red. Occasionally, it can also be used to display success messages. When it displays success message, the background color would be a pleasing green. Yep. This directive is designed to display both types of messages.

I like to show you how this directive is used in the main application. Then I can show how this directive is defined. In the file index.html, you will find something like this:

<div info-display op-success="vm.opSuccess" msg-to-show="vm.statusMessage" ></div>

Here, I added a div, and one attribute of this div is info-display. This info-display is the directive. When the application runs, AngularJS will compile and link this div with more HTML mark up. underneath it.

Notice the directive attribute is used as "info-display". This is an AngularJS convention. The directive is actually called infoDisplay. When you actually use it, the name is all lower case and words are connected by dash (or apostrophe). This is how AngularJS locates the directive from markup to the directive definition. Exactly how this is done I am not quite sure, but I do know the end result. Here is the markup of the alert stripe being displayed:

<div class="row ng-scope" ng-if="msgToShow != null && msgToShow.length > 0">
   <div class="col-xs-12">
      <div class="alert info-display ng-binding alert-success" ng-class="{ 'alert-success': opSuccess, 'alert-danger': !opSuccess }" ng-click="info.clickOnStatus()">User registration request has been processed successfully.</div>
   </div>
</div>

Obviously some type of run-time HTML injection is happening here. This means I have to defined the HTML mark up of the innjected component. In the file called static/assets/app/pages/infoDisplay.html, you can find the mark up of such injection. Here it is:

<div class="row" ng-if="msgToShow != null && msgToShow.length > 0">
   <div class="col-xs-12">
      <div class="alert info-display" ng-class="{ 'alert-success': opSuccess, 'alert-danger': !opSuccess }" ng-click="info.clickOnStatus()">{{msgToShow}}</div>
   </div>
</div>

This piece of markup is pretty simple, but there are a few interest points that should be discussed. First, ngIf or ng-if directive. It is used as:

<div class="row" ng-if="msgToShow != null && msgToShow.length > 0">
...
</div>

ngIf in this case is used to determine when this row div and its children will be displayed. The condition is that when the warning/error/success message is not null, and it has lengh greater than 0, then it will be displayed. what is cool with ngIf is that when the condition is false, the element is not added in the HTML DOM tree. This is ideal when you only want add DOM element at the appropriate time, and never have the element in the DOM tree when the time is not appropriate.

The other thing I want to show is the use of ngClass or ng-class directive. This is a useful directive. It can be used to conditionally add new CSS class. For this directive, I want to display the message in two different background colors, success message will have a light green color. This requires Bootstrap library's alert-success CSS class (combine with the alert class). And I want to display warning or error message with a red background. This requires the use of alert-danger class. The advantage of using ngClass allows one or the other to be added to the CSS class definition at runtime:

...
<div class="alert info-display" ng-class="{ 'alert-success': opSuccess, 'alert-danger': !opSuccess }" ng-click="info.clickOnStatus()">{{msgToShow}}</div>
...

Overall, this mark-up code for the directive is no different from the elements of any ui view for AngularJS controller. The difference is in the JavaScript code of the directive, which is showed in the next section.

The Definition of the Directive

In this section, I will show you how the directive is defined. Let me show you the whole source code. It is defined in the file called infoDisplay.js:

(function () {
   "use strict";
   var mod = angular.module("infoDisplayModule", [ ]);
   mod.directive("infoDisplay", [ function () {
      var infoDisplayController = ["$scope", function ($scope) {
         var info = this;
         
         info.clickOnStatus = function () {
            $scope.msgToShow = "";
            $scope.opSuccess = false;
         };
      }];
      
      return {
         restrict: "EA",
         templateUrl: "/assets/app/pages/infoDisplay.html",
         scope: {
            msgToShow: "=",
            opSuccess: "="
         },
         controller: infoDisplayController,
         controllerAs: "info"
      };
   }])
   ...
})();

This is only first part of the file. The second part of this file defines a service factory, which will be discussed in the next section. The above code snippet defines the directive. The way directive is defined can be explained in a few steps. The first step is the declaration of directive, as this:

var mod = angular.module("infoDisplayModule", [ ]);
mod.directive("infoDisplay", [ function () {

...
...

}])

This defines an AngularJS module called "infoDisplayModule". Then declare the directive called "infoDisplay" as part of this new module. The directive declaration takes the name of the directive and an array of depended components. In this class there is no other depended components. The dependency injection will be for the controller of the directive.

The next step is to have the directive return an object that contains all the info for AngularJS to render it. It is the return statement at the end of the directive declaration:

var mod = angular.module("infoDisplayModule", [ ]);
mod.directive("infoDisplay", [ function () {
   ...
   ...
   
   return {
      restrict: "EA",
      templateUrl: "/assets/app/pages/infoDisplay.html",
      scope: {
         msgToShow: "=",
         opSuccess: "="
      },
      controller: infoDisplayController,
      controllerAs: "info"
   };
   
}])

Looks simple, yet there are a couple things you must know. The first is the line restrict: "EA";. This is telling AngualarJS that the directive can only be part of a HTML tag as element (value "E") or as an attribute of an element (value "A"). The second line tells AngularJS where the HTML template (which is shown in previous section) can be found.

The third part is the scope property. This is for defining an isolated scope. It is not absolutely necessary. But an isolated scope allows multiple copies of the same directive to be placed on the same area, each can have a isolated scope to contain model data for its own display. The disadvantage is that they take more browser memory and runs slower. When you only need one copy of it on a specific area, and want it run fast, you can ignore adding the isolated scope. In this case, we cannot ignore it. We need the isolated scope so we can inject the data models from the host UI into this directive:

var mod = angular.module("infoDisplayModule", [ ]);
mod.directive("infoDisplay", [ function () {
   ...
   ...
   
   return {
      ...
      scope: {
         msgToShow: "=",
         opSuccess: "="
      },
      ...
   };
   
}])

The isolated scope has two injected data properties. One is called msgToShow. The other is called opSuccess. The values assigned to these two is the equal sign. This means the data model bounded to these scope data models are two way communitive. The change in the host will take effect of the bound data model almost immediately. The change of the data model in the directive takes effect in the host data model almost immediately as well. This is very useful for my diective because the host area should set whether the operation is successful or not, and what the message should be. And if the host resets these two, the warning stripe will disappear. From the directive's perspective, there is the ability for user to click the warning stripe and the stripe will disappear. This click will reset the two values, which take effect on the host area as well. The two way data values exchange ensures the clearing and value setting works back and forth seaminglessly. There are also one way binding and passing host controller methods references to the directive so directive can invoke the method directly. I won't cover them here.

The last part is to define a controller and define how the controller can access the view model properties for display. Remember the isolated scope. This will be injected into this controller as the $scope object. It is also the place to add the event handler for the click which the user can do to close the warning stripe. Here it is:

var mod = angular.module("infoDisplayModule", [ ]);
mod.directive("infoDisplay", [ function () {
   
   var infoDisplayController = ["$scope", function ($scope) {
      var info = this;
      
      info.clickOnStatus = function () {
         $scope.msgToShow = "";
         $scope.opSuccess = false;
      };
   }];
   
   ...
}])

The definition of this controller is a bit different. It is an array. The first is the dependency the controller needed, the $scope. The next one is the method that runs during initialization. In this initialization function, I get a reference of the this reference and assign to scoped reference variable called "info". This "info" reference will be used as the main view model. Then I define the click event handler called info.clickOnStatus() which will be bound to the ngClick for the warning stripe div.

The event handler resets the two-way bounded properties $scope.msgToShow and $scope.opSuccess. Remember the isolated scope? That is what this $scope is referencing. When these two properties are reset, the change also reflected in the host controller. Then the warning stripe will disappear because the ngIf directive that conditionally adds it to the HTML DOM tree and conditionally removes it.

Added Bonus: Factory As Service

In the same js file infoDisplay.js, there is also a declaration of an AngularJS factory. A factory is for creating an object. It is common to use factory to create service objects then the service objects can be used provide essential operations. I use the factory to create services that can be used in many places and remove duplicated code.

Before I go any further, I like to show the entire source code for the factory. Here it is:

...
.factory("infoDisplayService", [ function () {
   var svc = {
      initStatusDisplay: initStatusDisplay,
      setStatusDisplay: setStatusDisplay
   };

   function initStatusDisplay(vm) {
      if (vm != null) {
         vm.opSuccess = false;
         vm.statusMessage = null;
      }
   }
   
   function setStatusDisplay(vm, opSuccess, statusMsg) {
      if (vm != null) {
         vm.opSuccess = opSuccess;
         vm.statusMessage = statusMsg;
      }
   }
   
   return svc;
}]);
...

This factory creates an object which has two methods. The first is called initStatusDisplay(). It can be used to add two properties (vm.opSuccess and vm.statusMessage) to any view model. I will explain what a view model is in the next section. Once these two are added to the view model of a controller, then they can be used to two way bound to the directive. Again, it will be explained in the next section.

The second method is called setStatusDisplay(). This is the method that can be called whereever or whenever status message needs change. The method first checks to make sure the view model of the target controller is available, then it will change the values for vm.opSuccess and vm.statusMessage, properties of the view model. Now, everything for the design of the directive is in place, it is time to place it in the sample application and test it out.

The Sampele Application - How to Use this Directive

This is it. The last stretch. In this section, I like to discuss how the directive is integrated into the sample application. The HTML markup is defined in the file called index.html. We have already seen the mark up of the directive at the beginning of this tutorial:

<div info-display op-success="vm.opSuccess" msg-to-show="vm.statusMessage" ></div>

The directive is added to this div as a property. Remember the directive name is called "infoDisplay" by convention, when it is used in an HTML file, the directive name is converted to "info-display". The directive uses two way binding for two properties opSuccess and msgToShow. They are done with this:

<div ... op-success="vm.opSuccess" msg-to-show="vm.statusMessage" ></div>

By convention, the property opSuccess is referred as op-success, and the property msgToShow is referred as msg-to-show. They are bound to the view model's vm.opSuccess and vm.statusMessage. These two will not be found in the controller of the application. They are added to the view model by the service which was discussed in the previous section.

Now, let's take a look at the AngularJS application. Here is the entire code of the application:

(function () {
   "use strict";
   var mod = angular.module("testSampleApp", [ "infoDisplayModule" ]);
   mod.controller("testSampleController", [ "$scope", "infoDisplayService",
      function ($scope, infoDisplayService) {
         var vm = this;
         infoDisplayService.initStatusDisplay(vm);
         
         vm.userEmail = null;
         vm.userPassword = null;
         vm.userPassword2 = null;
         vm.userFullName = null;
         vm.userAddress = null;
         vm.userCity = null;
         vm.userState = null;
         vm.userZipCode = null;
         
         vm.doSubmit = function () {
            if (validateInput()) {
               infoDisplayService.setStatusDisplay(vm, true, "User registration request has been processed successfully.");
            }
         };
         
         vm.doClear = function () {
            vm.userEmail = "";
            vm.userPassword = "";
            vm.userPassword2 = "";
            vm.userFullName = "";
            vm.userAddress = "";
            vm.userCity = "";
            vm.userState = "";
            vm.userZipCode = "";
            
            infoDisplayService.setStatusDisplay(vm, false, "");
         };
         
         function validateInput() {
            if (vm.userEmail == null || vm.userEmail.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User email cannot be null or empty.");
               return false;
            }
            
            if (vm.userPassword == null || vm.userPassword.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User password cannot be null or empty.");
               return false;
            }
            
            if (vm.userPassword2 == null || vm.userPassword2.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "Re-typed user password cannot be null or empty.");
               return false;
            }

            if (vm.userPassword2 !== vm.userPassword) {
               infoDisplayService.setStatusDisplay(vm, false, "Passwords mismatch.");
               return false;
            }
            
            if (vm.userFullName == null || vm.userFullName.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User full name cannot be null or empty.");
               return false;
            }
            
            if (vm.userAddress == null || vm.userAddress.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User address line cannot be null or empty.");
               return false;
            }
            
            if (vm.userCity == null || vm.userCity.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User address city cannot be null or empty.");
               return false;
            }
            
            if (vm.userState == null || vm.userState.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User address state cannot be null or empty.");
               return false;
            }
            
            if (vm.userZipCode == null || vm.userZipCode.length <= 0) {
               infoDisplayService.setStatusDisplay(vm, false, "User address zip code cannot be null or empty.");
               return false;
            }
            
            return true;
         }
      }
   ]);
})();

Let's start with AngularJS module definition and the declaration of the controller for the application:

   var mod = angular.module("testSampleApp", [ "infoDisplayModule" ]);
   mod.controller("testSampleController", [ "$scope", "infoDisplayService",
      function ($scope, infoDisplayService) {
...
      }
   ]);

In this, I have to inject the module where the directive is defined. The module injected in is called "infoDisplayModule". For the controller, I will inject the service (infoDisplayService).

Next is the view model properties declaration:

   ...
   var vm = this;
   infoDisplayService.initStatusDisplay(vm);
   
   vm.userEmail = null;
   vm.userPassword = null;
   vm.userPassword2 = null;
   vm.userFullName = null;
   vm.userAddress = null;
   vm.userCity = null;
   vm.userState = null;
   vm.userZipCode = null;
   ...

I use "vm" as the view model reference, which point to the this reference of the object. The service infoDisplayService will create the opSuccess and statusMessage for the view model. Then a number of properties for the for user registration form.

When user hits the Submit button, this sample application will perform data validation of the properties of the view model. If any of these are not valid, the submission will trigger error message being displayed. If everything is OK, the success message will be displayed. The validation and display of the status messages via warning stripe is done the in event handler method of the "Submit" button.

   vm.doSubmit = function () {
      if (validateInput()) {
         infoDisplayService.setStatusDisplay(vm, true, "User registration request has been processed successfully.");
      }
   };

As you can see, the service will take care of setting the operation success and error/warning status message, Although I had to pass in the entire view model. But the service will only change the values of the two properties, vm.opSuccess and vm.statusMessage.

In this event handler, there is the validateInput() which performs the input values validation:

function validateInput() {
   if (vm.userEmail == null || vm.userEmail.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User email cannot be null or empty.");
      return false;
   }
   
   if (vm.userPassword == null || vm.userPassword.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User password cannot be null or empty.");
      return false;
   }
   
   if (vm.userPassword2 == null || vm.userPassword2.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "Re-typed user password cannot be null or empty.");
      return false;
   }

   if (vm.userPassword2 !== vm.userPassword) {
      infoDisplayService.setStatusDisplay(vm, false, "Passwords mismatch.");
      return false;
   }
   
   if (vm.userFullName == null || vm.userFullName.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User full name cannot be null or empty.");
      return false;
   }
   
   if (vm.userAddress == null || vm.userAddress.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User address line cannot be null or empty.");
      return false;
   }
   
   if (vm.userCity == null || vm.userCity.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User address city cannot be null or empty.");
      return false;
   }
   
   if (vm.userState == null || vm.userState.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User address state cannot be null or empty.");
      return false;
   }
   
   if (vm.userZipCode == null || vm.userZipCode.length <= 0) {
      infoDisplayService.setStatusDisplay(vm, false, "User address zip code cannot be null or empty.");
      return false;
   }
   
   return true;
}

As you can see, it is pretty straight forward, for every input property, is the value as expected, if the value is invalid, the error message will be displayed. The validation method will return false immediately. And this method will return false. When everything checks out, the method returns true.

How to Test Sample Application

After you downloaded the source code, please go to the folder and sub folders of src/main/resources/static/assets, then rename the *.sj files to *.js.

This is a Spring Boot based application, to compile the entire project, please run the following command in base folder where you can find the POM.xml:

mvn clean install

Wait for the build to complete, and it will succeed. Then use the following command to run the application:

java -jar target/hanbo-agular-warningbar-1.0.1.jar

Wait for the application to start up. Then open Chrome or Firefox, and navigate to the following url:

http://localhost:8080

When the page finishes loading, it will display all the comments structured in hierarchical order, like the first screenshot shown in this tutorial:

If you can see this, then the sample application is built successful. Feel free to put some input values and click the Submit button and see what the error or success message will be displaying.

Summary

In this tutorial, I present a very simple reusable component for AngularJS application. As I have observed, it is very common to have a a little div on the page displaying the warning, error, and success messages. Rather than having the same HTML or JavaScript codes duplicated on different part of an application, I have created this reusable component that can be used in places of these duplicated codes.

The tutorial explain in detail how the component, a directive, can be defined. How the controller's view model properties can be passed into the directive, what two-way binding is, how this directive utilize it. And how isolated scope is defined. These are strange concepts if you don't know AngularJS enough. As your experience with AngularJS increases, these concepts will not be as strange, and the concepts will be clear.

Lastly, the sample application serves as a reference program, showing you how this reusable component can be integrated into your application. And it is also a test program in case you modify the directive and needed something to test the changes. If you feel adventrous, you can modify the directive to server your development needs. To me, this is adequate for the next few projects. I hope you can find some use of it as well. Thanks for reading this tutorial.

History

  • 07/30/2020 - Initial Draft.

Add Comment

Comments