Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

AngularJS Get Started and Miscellaneous

5.00/5 (5 votes)
30 Aug 2015CPOL19 min read 22.9K   186  
This is a study note on Angular. I realized that Angular is a larger topic than I initially thought of. I hope I can be concise enough to cover a few interesting aspects in as few sentences as possible.

Introduction

This is a study note on Angular. I realized that Angular is a larger topic than I initially thought of. I hope I can be concise enough to cover a few interesting aspects in as few sentences as possible.

Background

When working on web applications, I found that it would be great if I can get the following capabilities either from Javascript itself or from some third party libraries:

  • Javascript is not like C# or Java, it does not natively support namespaces/packages. When a Javascript program gets reasonably complex, it is very common to have name collisions;
  • In a single page web application, Ajax calls are used extensively. It would be nice that I can easily get a JSON object with the data from the UI to send to the server. When an Ajax call receives the data from the server, it would also be nice if I can easily update the UI without working on each HTML element.

According to the Angular web site, Angular fits the requirements nicely. The module structure and the dependency injection mechanism solve the name collision problem, while the two way binding capability helps me to synchronize the UI and the data. In order to familiarize myself with Angular, I prepared this study note. Hopefully it will help you to understand Angular too.

Image 1

The attached file is a Java Maven project. If you use Java, it is great, and you can simply download it and import it into Eclipse to run it. If you do not use Java, it is not a problem. The project only has HTML files. You can just get the HTML files and load them into the browser. My recommendation is that it is better to put the HTML files on a web server and load it from the server to avoid the browser's security checks. You can use Tomcat, IIS, Node.js, and whatever servers that you are comfort with. The Javascript library for Angular in the examples is linked from the google CDN. If you want to run the examples by yourself, please make sure your computer has internet access, so your browser can download the Angular Javascript file. The "index.html" page contains the HTTP links to all the examples.

Image 2

The examples are not intended to serve as a comprehensive document for Angular. If you want the comprehensive document, you can go to the Angular web page.

1. basic-bootstrap.html

This example is to demonstrate how Angular takes control of the web pages or part of the web pages. In Angular terminology, this process is called "bootstrap". This example also tries to answer the following basic start-up questions:

  • Where is an Angular data model?
  • How is the data model related to the HTML elements?
  • How do the user activities like button clicks affect the data model and the UI?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    var myApp = angular.module('myApp', []);
    
    myApp.controller('MyController', ['$scope', function ($scope) {
        $scope.no = 0;
        $scope.data = {greeting: 'Hello World'};
        
        $scope.sayHello = function() {
            $scope.no = $scope.no + 1;
            $scope.data = {greeting: 'Hello World No.' + $scope.no};
        };
    }]);
    
    angular.element(document).ready(function() {
        var element = document.getElementById('divManuBootstrap-angular');
        angular.bootstrap(element, ['myApp'], {strictDi: true});
    });
    
    $(document).ready(function() {
        var element = document.getElementById('divManuBootstrap-jQuery');
        angular.bootstrap(element, ['myApp'], {strictDi: true});
    })
    
</script>
</head>
<body>
<div id="divAutomaticInitialization" ng-app="myApp" ng-strict-di>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<div id="divManuBootstrap-angular">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<div id="divManuBootstrap-jQuery">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

You may notice that this example linked both Angular and jQuery in the web page. According to the Angular documentation:

  • Angular can use jQuery if it's present in the app when the application is being bootstrapped;
  • If jQuery is not present in the script path, Angular falls back to its own implementation of the subset of jQuery that Angular calls jQLite.

Since we linked the "angular.min.js" file in the web page, it will be executed after the web page is loaded. It will create an object called "angular". The "angular" object is the primary reference that we can use to access the Angular functionalities. The "angular.module()" method creates an Angular module. We will talk about more about Angular modules in the next example. For now, we can just think of an Angular module being the angular way of grouping our code. 

  • In the above code, I added a controller to the "myApp" module. In Angular, a controller is a function that will be executed when binding the data model to the HTML UI elements;
  • In the standard Angular syntax, a controller will be passed in a variable "$scope". The "$scope" variable is the Angular data model that we can add both data and function properties to it.

We have two ways to bind the data model "$scope" to the HTML elements, they are automatic bootstrap and manual bootstrap.

  • The "DIV" element with id "divAutomaticInitialization" is automatically bootstrapped. In order to set up an automatically bootstrap, we need to specify the "ng-app" and "ng-controller" attributes to tell Angular which module and controller to use to bind to the HTML elements. In the HTML elements, we will also need to tell Angular which data or function property is associated to which HTML element. For example, the "sayHello()" function is associated to the button click event though "ng-click" attribute. The angular specific custom HTML attributes like "ng-app", "ng-controller", and "ng-click" are called directives;
  • The "DIV" element with id "divManuBootstrap-angular" is manually bootstrapped in the document's ready event captured by the Angular syntax "angular.element(document)". We can call the "angular.bootstrap()" method and tell it the HTML element and the Angular module to initiate the bootstrap;
  • The "DIV" element with id "divManuBootstrap-jQuery" is also manually bootstrapped. But it is bootstrapped in the document's ready event captured by the standard jQuery syntax "$(document)".

Image 3

By loading the example into a web browser and clicking on each of the buttons a couple of times, we will notice the following:

  • Angular does work well with jQuery. In this example, even we used the Angular syntax "angular.element(document)", we are virtually calling the jQuery to capture the document's ready event;
  • When we click on each of the "Say hello..." buttons, we will notice that the "$scope.sayHello()" function is called, and the data on the "$scope" is updated. Angular also helped us to have the text in the "<span>" element updated on the UI;
  • When we click on each of the "Say hello..." buttons, we can also notice that only the text next to the button is updated. This means that the "$scope" object is created for each controller binding, not for each controller. Although we have only one controller, but it is bound three times to three different "DIV" elements, so we have three independent "$scope" objects in the Angular context in the web page.

Image 4

If we open the firebug to take a look at the network traffic, we will see that Angular file size is almost twice as large as jQuery. Since Angular does use jQuery and also implements its own jQLite,  and since Angular document does mention that on relatively lower level operations, jQuery can do better job than Angular, I think that it would be nice if Angular can come up two versions, one with jQLite and one without it. If we want to use jQuery, we can choose the version without the jQLite, so the users will have a smaller Angular to download.

2.basic-module-and-di.html

Angular groups the application code in modules, which kind of serves the namespace/package functionalities in C# and Java, although it is verbose compared with the concise C# and Java. Angular also has a dependency injection mechanism, so the Angular objects are not created by the application code. Instead, they are injected by Angular when we need them.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    // Declare the modules
    var dataModule = angular.module('dataModule', []);
    var dataManipulationModuleByService = angular
        .module('dataManipulationModuleByService', ['dataModule']);
    var dataManipulationModuleByProvider = angular
        .module('dataManipulationModuleByProvider', ['dataModule']);
    var AppModule = angular.module('AppModule',
            ['dataModule', 'dataManipulationModuleByService', 'dataManipulationModuleByProvider']);
        
    // Add a factory to the 'dataModule' module
    dataModule.factory('appData', function() {
        return {no: 0, greeting: 'Hello World'};
    });
        
    // Add a service to the 'dataManipulationModuleByService' module
    dataManipulationModuleByService.service('appDataManipulatorByService', ['appData', function(data) {
        this.increaseGreeting = function() {
            data.no = data.no + 1;
            data.greeting = 'Hello World No.' + data.no;
        };
    }]);
        
    // Add a provider to the 'dataManipulationModuleByProvider' module
    dataManipulationModuleByProvider.provider('appDataManipulatorByProvider', function() {
        var greetingText = null;
        this.setGreetingText = function(text) { greetingText = text; };
        
        this.$get = ['appData', function(data) {
            return {
                decreaseGreeting: function() {
                    data.no = data.no - 1;
                    data.greeting = greetingText + data.no;
                }
            };
        }];
    }).config(["appDataManipulatorByProviderProvider", function(provider) {
        provider.setGreetingText('Hello World No.');
    }]);
        
    // Use the modules in the 'AppModule' module
    AppModule.controller('MyController',
            ['$scope', 'appData', 'appDataManipulatorByService', 'appDataManipulatorByProvider',
                 function ($scope, data, sManipulator, pManipulator) {
        $scope.data = data;
        $scope.addGreeting = function() {
            sManipulator.increaseGreeting();
        };
        $scope.decreaseGreeting = function() {
            pManipulator.decreaseGreeting();
        }
    }]);
    
    angular.element(document).ready(function() {
        angular.bootstrap(document.getElementById('divManuBootstrap-angular'), ['AppModule'], {strictDi: true});
    });
</script>
</head>
<body>
<div id="divAutomaticInitialization" ng-app="AppModule" ng-strict-di>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
</div>
    
<div id="divManuBootstrap-angular">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

Angular modules are created by "angular.module('module name', [An array of module names that this module depends on])" syntax. In this example, we have 4 modules.

  • The "dataModule" module has no dependency module;
  • The "dataManipulationModuleByService" depends on objects from the "dataModule" module;
  • The "dataManipulationModuleByProvider" dependes on objects from the "dataModule" module;
  • The "AppModule" depends on the "dataModule", "dataManipulationModuleByService", and "dataManipulationModuleByProvider" modules.

We have have 3 methods to add objects to an Angular module, namely factory, service, and provider. 

  • The "appData" object is added to the "dataModule" by the "dataModule.factory()" method. The function parameter passed to the "dataModule.factory()" method needs to return an object. The Angular DI mechanism will get this object by calling this function and associate it with the module;
  • The "appDataManipulatorByService" object is added to the "dataManipulationModuleByService" by the "dataManipulationModuleByService.service()" method. The function parameter passed to the "dataManipulationModuleByService.service()" needs to be a Javascript constructor function. The Angular DI mechanism will use this constructor function to create an object and associate it with the module;
  • The "appDataManipulatorByProvider" object is added to the "dataManipulationModuleByProvider" by the "dataManipulationModuleByProvider.provider()" method. The function parameter passed to the "dataManipulationModuleByProvider.provider()" method needs to be a constructor function. This constructor function needs to add the "$get" method that returns an object. The Angular DI mechanism will use the "$get" method to get the object that will be associated to the module.

When adding objects to the modules, we can inject objects already in the module or in other modules. The DI mechanism will inject these objects to the "factory()", "service()", and "$get()" methods.

  • To use objects in other modules, the current module needs to declare the other modules as dependencies;
  • With standard Angular syntax, we need to use "['$scope', 'appData', 'appDataManipulatorByService', 'appDataManipulatorByProvider', function ($scope, data, sManipulator, pManipulator)()" to inject objects. The "$scope", "appData", "appDataManipulatorByService", and "appDataManipulatorByProvider" are the names of the objects. They will be injected in the order that is declared.

Image 5

In this example, the "divAutomaticInitialization" is automatically bootstrapped, and the "divManuBootstrap-angular" is manually bootstrapped. In the "divAutomaticInitialization", "MyController" is bound 3 times to two different "DIV" elements. When we click the buttons, we should notice that the texts of the top two sections are always synchronized regardless if we click on the "Add hello..." or the "Decrease hello..." buttons. But the text in the last section changes by itself. 

  • When a controller is bound to an HTML element, an independent "$scope" object is created by Angular, the "$scope" object belongs to the binding;
  • The reason why the texts in the top two sections are synchronized is because the objects injected by Angular DI are singletons. You may notice that the "appData" object from the "dataModule" is injected to the "MyController". Although each binding of the controller creates its own "$scope", but the same "appData" object is injected for each binding;
  • The scope of the singletons is an Angular bootstrap. The "divManuBootstrap-angular" is independently bootstrapped, so Angular created another "appData" in its own bootstrap scope.
  • You can get more information about the scope of the Angular DI by looking at the Angular injector.

3.basic-two-way-binding.html

This example is intended to demonstrate Angular's two way binding capability.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        $scope.acceptance = function() {
            var score = $scope.score.id;
            
            if (! score)
                return 'Please select a score';
            else if (score === '80+') 
                return 'You are accepted to the school!';
            else if (score === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        };
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController">
        <select ng-options="option.name for option in scoreOptions track by option.id"
              ng-model="score"></select>
        <span>{{acceptance()}}</span>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • This is a simple example that we have only a single dropdown box and a text label "<span>";
  • The user's choice of the dropdown box is bound to the "$scope.score" from the UI to the data model;
  • If the data model is changed, the "$scope.acceptance()" function is called to calculate the acceptance status based on the given score. The acceptance status is then automatically shown in the text label through the Angular binding.
  • In a real life application, we can well use an Ajax call to the server to do some more complex calculations based on the number of applicants, and the numbers of the available slots, etc. But in this simple example, I simply hard-coded the acceptance logic.

Image 6

If we make changes to the selected values in the dropdown box, the acceptance status is automatically updated and shown on the UI. Remember that we did not write any code to check the selected value in the dropdown box and we did not write any code to update the text in the label. All the magic is done by the power of Angular.

4.basic-big-power-big-responsibility.html

This example is to extend the "3.basic-two-way-binding.html" to add some business meaning to see how Angular performs.

Image 7

  • The score is the primary acceptance criterion;
  • The other entries are just for information purpose;
  • If the score makes the student into the waiting list, the statement can help the school to make decisions.

The business logic in this example should make sense in most of the modern world, because race, gender, and body type really should not matter when accepting a student to the school. I know that in the United States, race and gender do matter in most of the cases, particularly to the best known schools like Harvard University. But to be consistent to the "all men/women are created equal" principle, and to be consistent to Martin Luther King's dreams, let me just use this simple business logic anyway.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        $scope.acceptance = function() {
            var score = $scope.score.id;
            
            console.log('This can be an ajax call ...');
            
            if (! score)
                return 'Please select a score';
            else if (score === '80+') 
                return 'You are accepted to the school!';
            else if (score === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        };
        
        // Add more data to the application
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        
        additionalInfo.race = null;
        additionalInfo.gender = null;
        additionalInfo.bodyType = null;
        additionalInfo.statementOfPurpose = null;
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>&nbsp;{{acceptance()}}</span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" ng-model="additionalInfo.race" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" ng-model="additionalInfo.gender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" ng-model="additionalInfo.bodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

If you load the example in your web page, you should notice that it works very well. When you select a score, your acceptance status is shown to you right away. Now let us open the firebug and take a look at the Javascript console.

Image 8

  • You will immediately see that the "$scope.acceptance()" function is called thousands of times. Remember that this example is just a simple example. In really application, we will make an Ajax call to the server to check the number of applicants and the number of available slots to make the acceptance decisions, so each call is in fact an Ajax call;
  • The Ajax call is made whenever you perform any action on the UI, when you type in your race, type in your statement. If you want to increase your chance of acceptance and write a long statement, the Ajax call is made for your every keystroke. For each of your keystroke, the "$scope.acceptance()" is called at least twice. your can easily let the application to make thousands of useless duplicate trips to the web server, and most likely the database server too;
  • We all know that President Obama's web site went dead because people went there to send health insurance applications. I read from the news that one keystroke on his web site will issue some Ajax calls to his web server, so the server quickly went down. I am not sure if they used Angular to build the web site, although I can easily find it out using firebug or other web development tools.

The reason of this virtually "Denial-of-service attack" type of behavior is due to the fact that Angular can provide us powerful bindings, but it does not know our business logic. It does not know the fact that in the modern world, we strongly believe in "all men/women are created equal". The race, gender, and body type has absolutely nothing to do with the acceptance. Without the knowledge of the business logic, angular took a simple approach to be powerful, it calls the "$scope.acceptance()" function at least twice whenever we make a single keystroke or possibly just a mouse leaving the text boxes. If you want to know why angular calls the "$scope.acceptance()" at least twice but not once for every keystroke, you can check out the Angular digest loop.

  • Big power should come with big responsibilities;
  • I am not trying to say that Angular encourages useless "Denial-of-service attack" type Ajax calls to shut-down your own web servers and the database servers, but if we do not use Angular right, it is very likely;
  • In the normal QA process, this problem may not be easily detectable since the QA server is normally lightly loaded. If the QA personals do not keep the firebugs open, the problem can be very well go to the production, where your production servers will get into the similar situation as President Obama's servers. 

5.basic-the-watcher.html

This example is intended to address the problem that we see in the "4.basic-big-power-big-responsibility.html".

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        var defaultAcceptance = 'Please select a score';
        $scope.acceptance = defaultAcceptance;
        
        $scope.$watch('score', function() {
            var score = $scope.score.id;
            
            if (score == '') {
                $scope.acceptance = defaultAcceptance;
                return;
            }
            
            console.log('This can be an ajax call ...');
            
            if (score === '80+') 
                $scope.acceptance = 'You are accepted to the school!';
            else if (score === '60+')
                $scope.acceptance = 'You are in the waiting list';
            else
                $scope.acceptance = 'You are rejected';
            
            
        });
        
        // Add more data to the application
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        
        additionalInfo.race = null;
        additionalInfo.gender = null;
        additionalInfo.bodyType = null;
        additionalInfo.statementOfPurpose = null;
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>&nbsp;{{acceptance}}</span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" ng-model="additionalInfo.race" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" ng-model="additionalInfo.gender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" ng-model="additionalInfo.bodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

This example is virtually the same as the "4.basic-big-power-big-responsibility.html".

  • Instead of using a function, it added a variable "$scope.acceptance" to bind it to the UI to report the acceptance status;
  • A watcher is add to the "$scope.score". When the score selection changes, the watcher function will fire to re-calculate the acceptance status.

If you run this example and set up your firebug, you will notice that the watcher function is only called when the score changes.

  • If Angular is not used properly, it can be very error prone, but if you follow the best practices, it can perform normally;
  • The fundamental of the best practice is that it is still the programmer's responsibility to tell when he/she wants the program to do something and not to do something. Regardless how powerful the framework is, there is no way to remove this duty from the programmers. Over these many years, the programming environment has changed significantly. But what has not been changed is the definition of a computer programming, which is "algorithm and data structure". Programmers still need to know the fundamentals. There is no way to remove this duty from the programmers by simply using a powerful framework, because any power comes with a cost and a responsibility.

6.basic-filter.html

This example is to demonstrate Angular's filter capability.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        additionalInfo.statementOfPurpose = null;
    }])
    
    module.filter('checkStatus', function() {
        return function(score) {
            var scoreId = score.id;
            
            console.log('This can be an ajax call ...');
            
            if (! scoreId)
                return 'Please select a score';
            else if (scoreId === '80+') 
                return 'You are accepted to the school!';
            else if (scoreId === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        }
    });
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>{{score|checkStatus}}</span>
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

We have seen from the example "4.basic-big-power-big-responsibility.html" that sometimes Angular can create undesirable situations. Now let us see how filter works.

  • This example created a filter function called "checkStatus";
  • We can simply bind it to the UI with the "{{score|checkStatus}}" syntax to report the acceptance status.

Image 9

We can then load the example page into the browser and select a score. We can also type in some statement. Everything looks normal and functional. We may hope that the Ajax calls will not fire for every key stroke, because the "{{score|checkStatus}}" syntax explicitly tells Angular to filer the "score" only. Let us now look at the firebug console.

Image 10

But unfortunately, we again got thousands of Ajax calls, at least two Ajax calls for each keystroke. This example shows us that we will need to use Angular filters with caution. It can fire when you do not intend it to fire.

7.basic-oops-what-is-going-on.html

This example shows an unstable situation if we do not use Angular properly. This situation can occur in any environment, not just Angular. But now let us see how it can happen in Angular.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        $scope.first = 0;
        $scope.second = 0;
        
        $scope.increaseFirst = function() {
            $scope.first++;
        }
        
        $scope.$watch('first', function() {
            if ($scope.first == 0) return;
            $scope.second--;
        });
        
        $scope.$watch('second', function() {
            if ($scope.second == 0) return;
            $scope.first++;
        })
        
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController">
        <div>First: {{first}}</div>
        <div>Second: {{first}}</div>
        <div><button type="button" ng-click="increaseFirst()">Increase First...</button></div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • We added two variables "first" and "second" to the "$scope";
  • A watcher function is used to watch the change of the "first" variable. When it changes, we decrease the "second" variable by 1;
  • A watcher function is used to watch the change of the "second" variable. When it changes, we increase the "first" variable by 1;
  • The "$scope.increaseFirst()" function is bound to the click event of the button to increase the "first" variable by 1.

Image 11

Load the example into the web browser and click on the "Increase First..." button, you can see that the numbers stopped at 12. Now let us take a look at the firebug console.

Image 12

  • Any experienced programmer should have noticed that we had an infinite loop which can happen in any environment, not just in Angular;
  • In Angular, because the digest loop has to run at least twice for a single change of a variable on the "$scope" to achieve the kind of power that Angular provides us, we need to be very careful to modify the values of the data in an Angular controller, particularly when you have a larger problem. When the data model has many data variables, the infinite loop condition will not be as easily identify as this simple example when we have only two variables.

8.basic-a-bare-javascript-solution.html

This is an example to achieve the same as the "4.basic-big-power-big-responsibility.html". It does not use Angular, it does not even use jQuery. I wanted to figure out how difficult life can be without Angular. I almost wanted to skip this example, because I really did not have time to full test it. But I finally put it here as a reference. If you want to use any code in this example, please make sure to test it yourself. In order to achieve something similar to the two-way binding, I created a Javascript file "simple-mapper.js".

var simplemapper = function(model) {
    var data = model.data;
    var elementMapping = model.elementMapping;
    
    return {
        serialize: function(item) {
            var mapping = elementMapping[item];
            var e = document.getElementById(mapping.element);
            data[item] = (mapping.type == 'value')? e.value: e.innerHTML;
        },
        deserialize: function(item) {
            var mapping = elementMapping[item];
            var e = document.getElementById(mapping.element);
            if (mapping.type == 'value')
                e.value = data[item];
            else
                e.innerHTML = data[item];
        },
        serializeMultiple: function(itemArray) {
            var len = itemArray.length
            for(var i = 0; i < len; i++){
                var item = itemArray[i];
                
                var mapping = elementMapping[item];
                var e = document.getElementById(mapping.element);
                data[item] = (mapping.type == 'value')? e.value: e.innerHTML;
            }
        },
        deserializeMultiple: function(itemArray) {
            var len = itemArray.length
            for(var i = 0; i < len; i++){
                var item = itemArray[i];
                
                var mapping = elementMapping[item];
                var e = document.getElementById(mapping.element);
                if (mapping.type == 'value')
                    e.value = data[item];
                else
                    e.innerHTML = data[item];
            }
            
        },
        serializeAll: function() {
            for(var item in elementMapping){
                this.serialize(item)
            }
        },
        deserializeAll: function() {
            for(var item in elementMapping){
                this.deserialize(item)
            }
        },
        getUpdatedDataItem: function(item) {
            this.serialize(item);
            return data[item];
        },
        getUpdatedData: function() {
            this.serializeAll();
            return data;
        }
        
    };
};
  • The "simplemapper()" function is to create the two way mapping utility object, it takes your data model;
  • The data model has two objects, the "model.data" object is the data part, and the "model.elementMapping" object tells the mapping object how to map the data to the UI elements;
  • The "serialize(item)" method updates the data from the UI element for the particular "item", while the "deserialize(item)" updates the UI element from the data for the particular "item";
  • I create a couple of "serialize()" and "deserialize()" methods for different granularities.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="scripts/simple-mapper.js"></script>
    
<script type="text/javascript">
    var appModel = function() {
        return {
            data: {
                score: '',
                acceptance: 'Please select a score',
                race: '',
                gender: '',
                bodyType: '',
                statementOfPurpose: ''
            },
            elementMapping: {
                score: {element: 'selScores', type: 'value'},
                acceptance: {element: 'spanAcceptance', type: 'html'},
                race: {element: 'txtRace', type: 'value'},
                gender: {element: 'txtGender', type: 'value'},
                bodyType: {element: 'txtBodyType', type: 'value'},
                statementOfPurpose: {element: 'txtStatementOfPurpose', type: 'value'}
            }
        };
    }();
    
    var controller = function(model) {
        var sm = simplemapper(model);
        var data = model.data;
        
        return {
            mapper: sm,
            checkAcceptance: function() {
                var score = sm.getUpdatedDataItem('score');
                
                console.log('This can be an ajax call ...');
                
                if (score == '')
                    data.acceptance = 'Please select a score';
                else if (score === '80+') 
                    data.acceptance = 'You are accepted to the school!';
                else if (score === '60+')
                    data.acceptance = 'You are in the waiting list';
                else
                    data.acceptance = 'You are rejected';
                
                sm.deserialize('acceptance');
            },
            checkSerialization: function() {
                console.log(sm.getUpdatedData());
            }
        }
    }(appModel);
    
    
    window.onload = function() {
        controller.mapper.deserializeAll();
    }
    
</script>
</head>
<body>
<div>
    <div id="biggerapp">
        <div>
            <select id="selScores" onchange="return controller.checkAcceptance()">
                <option value=''>Please select ..</option>
                <option value='80+'>80 and above</option>
                <option value='60+'>60 - 79</option>
                <option value='60-'>Below 60</option>
            </select>&nbsp;<span id="spanAcceptance"></span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" id="txtRace" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" id="txtGender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" id="txtBodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" id="txtStatementOfPurpose"></textarea>
        </div>
        <div>
            <button type="button" onclick="return controller.checkSerialization()">
                Check serialization</button>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • The "appModel" object is the application's data model. The "appModel.data" is the application's data, the "appModel.elementMapping" tells the mapper object how to map the data to the UI elements;
  • The "element" entry for the "elementMapping" is the "id" of the HTML element on the UI, and the "type" entry tells if the data is the value of the HTML element or the innerHTML of the HTML element;
  • The "controller" object is created based on the "appModel" object. It has its own "simplemapper" built from the "simple-mapper.js", it also exposes the "checkAcceptance()" and "checkSerialization()" methods;
  • The "appModel" and the UI elements are bound together though the "controller" in the "window.onload" event.
  • You may notice that we actually did not write too much more code in "8.basic-a-bare-javascript-solution.html" to achieve the same functionality compared with the "4.basic-big-power-big-responsibility.html".

Image 13

If you click on the "Check serialization" button and take a look at the Javascript console in your firebug, you should find that all the data you put into the UI is serialized to your data model. The following is from firefox and I re-typed the text as above when I tried to show them in firebug, I made a typo for "1/1 W 1/1 B ....", I hope you do not mind. You can run the example by yourself anyway.

Image 14

You can also check if there are any unintentional Ajax calls. Of course no, because the program has absolute control on when the Ajax call should be made.

Points of interests

  • This is a study note on Angular. I realized that Angular is a larger topic than I initially thought of;
  • I did not intend to make it so long, but it ended up so long and I apologize for it being so long;
  • Angular is a nice tool/framework that helped me to solve the two problems that I mentioned at the beginning, name collision in Javascript and data model binding to HTML elements;
  • Angular is a powerful tool/framework. To use it well, you need to strictly follow the best practices and you may also need to keep finding out more best practices from your own mistakes;
  • It is a good idea to keep your firebug or other development tools open, and it is a good idea to print out some debug information in your Javascript console to watch any undesirable behaviors and correct them as quickly as possible. Some problems may not be easily detectable by the QA people;
  • I hope you like my postings and I hope this study note can help you one way or the other.

History

First Revision - 8/30/2015

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)