Source: AngularJS: Get Started, K. Scott Allen (PluralSight)

https://angularjs.org

Introduction

AngularJS is a Javascript library written in Javascript and distributed as a Javascript file. The Javascript file can be downloaded or loaded via CDN.

AngularJs At A Glance

  • Module is like a container used for grouping AngularJS code together. Controllers, views, services, directives, etc that belong to a module are all compiled together.

    To use an external module, e.g ngAnimate, https://code.angularjs.org/1.2.32/angular-animate.js, you must include the module as dependency in your app.
    let myApp = angular.module('myApp', ['ngAnimate']);

  • Config is a kind of global namespace in AngularJs. Anything including special configurations that you want available to the entire app goes here. You can put constants here. This is a good place to put AngularJs services.
  • Routes map views and controllers for a particular URL
  • Scope glues together views and controllers
  • Directives - an instruction in AngularJs to manipulate a piece of the DOM. Directives are passed as custom attributes to html tags.

    Example AngularJs directives
    ng-app, ng-controller, ng-show
    <div ng-controller='mainController'></div>

  • Services used to augment controllers. Angular service variable names start with a $ sign. Any service that you would like injected in your controller must be passed as parameters when declaring a controller, myApp.controller('studentsController', function($scope, $route, $otherServices) { //... });
  • View binds to scope
  • Model refers to whatever data object is on scope

$compile

Before an Angular app is compiled, the DOM must load first and fire the onContentReady() event.

  • Angular looks for the ng-app directive on the DOM
  • If found, it compiles the Module referenced by the ng-app directive
  • During compilation, templates and directives are bound together. The injector handles dependency injection.

$digest and $apply

Digest and apply are central to how Angular knows if anything has changed in the App or DOM. $Digest loop is the equivalent of JS event loop.

  • $digest processes all watchers on the current scope
  • $apply is used to notify that something has changed outside the AngularJs domain (e.g jQuery changed something on page). $apply then forces a $digest cycle.

Controller and $scope

$scope glues Controllers and Views together. The controller constructs the model on $scope and provides commands for the View to act upon. $scope provides context.

Best Practice

  • controllers must never manipulate the DOM (directives do that)
  • controllers must be small and focused
  • controllers should not talk to other controllers. Uses services to share information between controllers.

Model and Services

  • Services are consumed via the dependency injection system, e.g. myApp.controller('mainController', function ($log, $scope, $filter){}); where scope, log and filter are services being injected into the mainController.
  • Services are application singletons. One service per application
  • Services are instantiated lazily
  • Services make automated testing easier

Routes

Used for linking urls to Controllers and Views. Define routes using $routeProvider. Typically used in conjunction with ngView directive and $routeParams service.

To indicate that an html file is going to use AngularJS, the ng-app directive must be specified. If specified on the html element, then the whole document will AngulaJs aware, if specified on a specific tag, then only that tag will be AngularJs aware.

Basic AngularJs Html Page

  • must reference angular.js file
  • must include ng-app directive
  • there can only be one ng-app directive per page
  • the ng-app directive can take an Angular module as parameter <body ng-app="moduleName">

See how it works at https://www.w3schools.com/angular/

<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
</head>
<body>
    <div ng-app="">
        <p>Client Details:</p>
        <p>Firstname: <input type="text" ng-model="firstname" placeholder="First name"></p>
        <p>Suname &nbsp;&nbsp;: <input type="text" ng-model="surname" placeholder="Surname"></p>
        <h3>Hi {{firstname}} {{surname}} </h3> 

    </div>
</body>
</html>

Load the page in a browser and as you type in the first name and surname, the details are displayed in H3 tag while you type.

Sample output


        Client Details:
Firstname:
Suname   :
Hi Sue Li

Angular uses double curly braces to bind expressions. It also posible to perform simple maths with number, e.g {{ (3 * 5) / 4 }}

AngularJS Controllers

Controllers build a model, which contains the data that we need to work with.

  • can have more than one controller on a page
  • controllers can be nested within another
  • Angular does not allow controllers in the global namespace. Controllers must be wrapped within a model

The ng-controller directive is used to specify the name of the controller to be used on the page.

    <div ng-app="">
        <div ng-controller="MainController">
            <p>Input something in the input box:</p>
        </div>
    </div>

ng-app directive must be available on the page for ng-controller to be recognised

A Javascript function MainController must exist to support the controller specified in <div ng-controller="MainController">

Sample MainController function


var MainController = function ($scope) {
    var person = {
        firstname: "Lucas",
        surname: "Don",
        imgurl: "/img/icon.png"
    }

    $scope.message = "Hello from main controller";
    $scope.person = person;
}

Sample MainController - Minifier Safe


app.controller('MainController', ['$scope', '$log', function($scope, $log){
    var person = {
        firstname: "Lucas",
        surname: "Don",
        imgurl: "/img/icon.png"
    }

    $scope.message = "Hello from main controller";
    $scope.person = person;    
}]);

/** Why does it work?
 * - A Javascript array accepts a function as data, [1, 2, function(){ console.log('Done!');}]
 * - Functions even in arrays are annotated and minified
 * - values are not minified ['$scope', '$log'] are treated as values. Variables are minified
 * - the order of parameters is important when using this syntax
 */

Angular invokes Controller functions automatically when it needs to create a controler to manage an area of the web page.

Similar to the ng- prefix, $ sign in parameter names (JS controller functions) signals that a Javascript function is in fact an Angular component.

Angular models are assigned to the controller parameter $scope in above example. The $scope variable it's self is not the model, but the items attached to it become the model. In above example, message is a variable that can now be attached to our html page.

Angular Modules

NB: The term module is heavily overloaded in Javascript.

  • Angular controllers must not be declared on the global namespace. Instead, controllers must be contained within a modules.
  • Angular module are therefore like a container for Controllers. A single module can contain any number of controllers.
  • An Angular application can have many modules depending on application complexity

You create a module by creating a variable referencing angular.module('moduleName', [moduleDependencies]) function. The order of dependent modules is important. If controller names clash, the controller in the dependent module that was declared first takes precedence. See example below

app.js - Declaring a module


// sample.js
// module app that depends on other modules, app2 and app3, all declared in the same file
angular.module('app', ['app2', 'app3']);
angular.module('app2', []);
angular.module('app3', []);

// create controller1 in module app3
angular.module('app3').controller('controller1', function($scope) {
    $scope.name = 'controller1 in app3';
});

// create controller1 in module app2
angular.module('app2').controller('controller1', function($scope) {
    $scope.name = 'controller1 in app2';
});

// the html
<body ng-app='app'>
    <div ng-controller='controller1'>      </div>
</body>
<!-- output
// angular.module('app', ['app2', 'app3']);
app2 is called first therefore "controller1 in app2" will be displayed
-->    

As part of restricting variables declared on the Javascript global namespace, an Angular module can be declared to be the only global variable.

app.js - Module as only variable on global namespace


// myApp must be the only global variable for your Angular app
var myApp = angular.module("myApp", []);

myApp.controller('mainController', function($scope) {
    $scope.message = 'Message from mainController';
});

$injector

The angular injector object contains a list of all objects within your application. Only 1 injector can exist in each angular application.

The reason controller1 in app2 is returned above has to do with the injector as the injector has registered the first available controller1 and there can only be one controller with that name in an injector.

There are 3 steps to working with modules. First two steps take place in Javascript, and the last on html pages

// Step 1. create a module called githubViewer
// Ommit brackets if you are referencing a module already defined, e.g third party module
var app = angular.module('githubViewer', []);

// Step 2. register your controller to the module
app.controller('MainController', MainController);

Tell Angular which module to use on html page

<!--
// Register module with Angular, on html page
// Angular will look for controllers in githubViewer module
-->

<body ng-app="githubViewer"> </body>

Creating a module

Module definition is contained in file app.js, the controller is in main.js and the html code in index.html

app.js - Module Code


/**
 * Angular module
 */

// declare module
var app = angular.module('githubViewer', []);

// register controllers here - ('NameOfController', FunctionToUse)
app.controller('MainController', MainController);


main.js - Controller Code


var MainController = function ($scope) {
    var person = {
        firstname: "Lucas",
        surname: "Mangwana",
        imgurl: "/img/icon.png"
    }

    $scope.message = "Hello";
    $scope.person = person;
}


Page html code

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
    <script src="main.js"></script>
    <script src="app.js"></script>    
</head>
<body ng-app="githubViewer">
    <div ng-controller="MainController">
        <p>Client details</p>
        <p>Firstname: <input type="text" ng-model="person.firstname"> </p>
        <p>Surname: <input type="text" ng-model="person.surname"> </p>
        <p> {{message}} {{ person.firstname }} {{ person.surname }}  </p>
    </div>
</body>
</html>

Sample output


    Client details 
Firstname:
Surname:
Hello Lucas Mangwana

Views and Directives

  • Data attached to the $scope variable is called model, $scope.firstname = 'Simba';. Firstname is the model, and models can be functions too $scope.getUser(id)
  • Use binding expressions to make models appear on html page {{ firstname }}
  • Angular uses directives to handle interaction between views and models.

ng-click Directive

  • used to handle the click event

Sample form with ng-click. The search button has a function search(nickname) that takes a nickname entered by user and searches an array of users. Displays the user's full name or "not found" message if there is no match.

<!-- index.html -->
<html lang="en">
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
    <script src="main.js"></script>
    <script src="app.js"></script>
</head>
<body ng-app="githubViewer">
    <div ng-controller="MainController">
        <form name="myForm">
            <p>Client details</p> 
            <p>Nickname: <input type="text" ng-model="nickname" placeholder="Search nickname">  </p>
            <p>Full Name: <input type="text" value="{{user.name || error.user}}" disabled> </p>
            <p>Click to search for {{nickname}} <input type="button" value="Search" ng-click="search(nickname)"></p>
        </form> 
    </div>
</body>
</html>

Project module code

// ./app.js
// declare module
var app = angular.module('githubViewer', []);

// register controllers here - ('NameOfController', FunctionToUse)
app.controller('MainController', MainController);

Code for MainController containing array of users and search function.

./main.js - MainController


// ./main.js
var MainController = function ($scope) {
    var person = {
        firstname: "Lucas",
        surname: "Mangwana",
        imgurl: "/img/icon.png"
    };

    var users = [
        {nickName: "sue", name: "Susan Boyle"},
        {nickName: "pat", name: "Pat Shange"},
        {nickName: "don", name: "The Donald"},
        {nickName: "fau", name: "The Doctor"},
        {nickName: "cov", name: "Self Isolation"}
    ];

    $scope.search = function(nick) {
        $scope.user = undefined;
        $scope.error = {user: 'not found'};

        if (typeof nick !== "string" || nick == "")
            return false; // invalid search string

        nick = nick.toLowerCase();
        users.forEach(function(u) {
            if (u.nickName.toLowerCase() == nick) {
                $scope.user = u;
                return true;
            }
        });
    };

    $scope.message = "Hello";
    $scope.person = person;
}

./ng-click: Sample Output


    Client details
Nickname:
Full Name:
Click to search for pat

AngularJS Routing

The ngRoute module is stored in its own Javascript file which must be referenced

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-route.js"></script>

To work with routes,

  • add ngRoute as a module dependency
  • add ng-view directive to html page

Each route has an html view defined by a chuck of html and the ng-view directive tells Angular where to place the chunk of html.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Angular Routes</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-route.js"></script>
</head>

<body ng-app="app">
    <div ng-view></div> <!-- view for each route will be displayed here -->
</body>

</html>

AngularJs Services

Most what we inject into controllers are services. Services are mostly singletons with the exception of $scope. Custom services that you create will be singletons.

Sample Services



// create a service, return the length of a fullName
// Services are singletons except $scope
app.service('nameService', function () {
    var self = this;
    this.fullName = 'Tim Shaw';
    this.nameLength = function () {
        return self.fullName.length;
    }
};

// use nameService in mainController
// Inject the nameService into mainController
app.controller('mainController', function($scope, $log, nameService) {
    $log.log(nameService.fullName); // Tim Shaw
    $log.log(nameService.nameLength()); // 8

    // set in scope
    $scope.fullName = nameService.fullName;

    // watch change in scope variable and update the variable in service
    $scope.$watch('fullName', function() {
        nameService.fullName = $scope.fullName;
    });
});

// use nameService in secondController
// mainController and secondController now share data through nameService
app.controller('secondController', function($scope, $log, nameService) {
    $log.log(nameService.fullName); // Tim Shaw
    $log.log(nameService.nameLength()); // 8

    // set in scope
    $scope.fullName = nameService.fullName;

    // watch change in scope variable and update the variable in service
    // without the watch changes will not reflect in both controllers
    $scope.$watch('fullName', function() {
        nameService.fullName = $scope.fullName;
    });
});


Creating Angular Directives

Views and controllers never interact directly.

$http service

Service used for making http calls (GET, POST, PUT, DELETE). Used to write / retrieve data from a database for example.

Sample Http Service


// Using http service
// $http parameter is required
var PersonController = function ($scope, $http) {
    // get user data (get request). Don't do this. Use promises instead
    $scope.user = $http.get("/user/100");

    // using promises
    $http.get("/user/100").then(
        // on success callback
        function(response){
            $scope.user = response.data; // 
        },

        // response failed callback
        function (reason) {
            $scope.error = "Record not found!";
        }
    );
}

Another example using $http module

// Using http service
// must inject $http module into the controller
// ... just showing alternative syntax

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

myApp.controller('PersonController', ['$scope', '$http', '$filter' function($scope, $http, $filter) {
    // showing filter use
    $scope.handle = '';
    $scope.lowercaseHandle = function () {
        return $filter('lowercase')($scope.handle);
    };

    // get request
    $http.get('/user/100')
        .success(function (response){
            $scope.user = response;
        })
        .error(function (err, status) {
            $scope.error = err;
            $scope.errorStatus = status;
        });

    // post request - write to database
    $http.post('/user/100', {
        firstname: $scope.firstname,
        surname: $scope.surname,
        age: $scope.age
    })
    .success(function (result) {
        $scope.user = result; // send result back to page
    })
    .error(function (data, status) {
        $scope.error = data;
    });

}]);

AngularJs Code Examples - app.js



// Angular module for noteApp
let noteApp = angular.module('noteApp', []);

// Custom service, store boards that can be shared by different controllers
// boardsService.getboards()
noteApp.factory('boardsService', function() {
    let boards = [
        {id: "01", title: "Title 1", content: "some text one"},
        {id: "02", title: "Title 2", content: "some text two"},
        {id: "03", title: "Title 3", content: "some text three"}
    ];

    return {
        getBoards : function() {
            return boards;
        }
    };
});

// Controller using $scope, and boardsService
noteApp.controller('mainController', [
    '$scope', '$filter', '$log', '$timeout', '$http', 'boardsService',
    function($scope, $filter, $log, $timeout, $http, boardsService) {
        $scope.boards = boardsService.getBoards();
        $scope.firstname = '';

        // Use ng-if/ng-show directive
        // to check that firstname is greater than  maxCharacters
        $scope.minCharacters = 2;

        // $filters: firstname to lowercase
        // Format a given value to, "upercase, lowercase, currency, date, json, limitTo, OrderBy "
        // https://docs.angularjs.org/api/ng/filter
        $scope.lcaseFirstname = function () {
            return $filter('lowercase')($scope.firstname);
        };

        // Digest loop: $watchers
        // Watch for models ($scope values) changing
        // $watchers keep track of newValue and oldValue for every model declared
        $scope.$watch('firstname', function (newValue, oldValue){
            $log.info('Firstname value changed');
            $log.log('New value: ' + newValue);
            $log.log('Old value: ' + oldValue);
        });

        // Digest loop: Outside AngularJs scope
        // If changes are not made within the AngularJs context,
        // the digest loop will not update everything as it should
        // Code below will acknowledge that scope changed but will not update the DOM
        // because the digest loop is never started
        setTimeout(function () {
            $scope.firstname = 'Lucas';
            console.log('Scope changed 1');
        }, 2000);

        // Digest loop: Outside AngularJs scope
        // Forcing digest loop, use $scope.$apply()
        // The DOM will be updated to show firstname: Lucas
        // Do this especially when you use external libraries like jQuery to manipulate the DOM
        setTimeout(function () {
            $scope.$apply(function () {
                $scope.firstname = 'Lucas';
                console.log('Scope changed 2');
            });
        }, 3000);

        // Digest loop: Within AngularJs scope
        // Alternatively you can inject and use AngularJs' $timeout service
        // $scope.$apply() is not required within AngularJs scope
        $timeout(function () {
                $scope.firstname = 'Luke';
                console.log('Scope changed 3');
        }, 4000);

        // Handler for ng-click directive
        $scope.clickedMe = function() {
            alert("I have been clicked!");
        };

        // $http service
        // Use $http service to get data from API endpoint
        $http.get('/api/students/get')
            .success(function (result) {
                $scope.rules = result;
            })
            .error(function (data, status) {
                console.error(data);
            })
        //

        // For use with $http.post()
        // Add new student details from html form
        $scope.newStudent = '';
        $scope.addStudent = function () {
            // $http service
            // Post data to Api endpoint
            $http.post('/api/students/save', { newStudent: $scope.newStudent })
                .success(function (result) {
                    $scope.rules = result;
                    $scope.newStudent = '';
                })
                .error(function (data, status) {
                    console.error(data);
                })
            //
        };
    }
]);

// Custom directive
// Useful when you have repeating block of html as often seen on search results
// if replace is true, the directive is replace with html from template
// Directive names are normalised to with a '-' wherever a capital letter occurs
// 'searchResults' becomes 'search-results' on html page.
noteApp.directive('searchResults', function () {
    return {
        // force users to call directive only as 'A' attribute, 'E' Element, 'C' class, 'M' comment.
        // 'AE' are turned on by default
        // Turn on all forms of calling a directive on html
        restrict: 'AECM',

        // on html page, directive can be element , or attribute 
// this will be replace by html defined on template variable // use templateUrl: '/page/file.html' if template is in a separate html page. template: '<a href="#" class="list-group-item"><h4>Custom Directive - search-results</h4><h5>Jane Logan</h5><div>1 Mulican Street, Hanover Way</div><small ng-transclude></small></a>', // replace directive with html from template replace: true, // allow transclusion, is false by default transclude: true, // enforce isolated scope scope: { // @ represents a text data being passed to scope variable // = represents two way binding studentName: '@', studentObject: '=' } } });

index.html for app.js above. Put the two files in the same folder to test

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.7.9/angular.min.js"></script>
    <script src="app.js"></script>
    <script src="//code.jquery.com/jquery-3.x-git.min.js"></script>
    <script src="//cdn.usebootstrap.com/bootstrap/latest/js/bootstrap.bundle.min.js"></script>
    <link rel="stylesheet" href="//cdn.usebootstrap.com/bootstrap/latest/css/bootstrap.min.css">
</head>
<body ng-app='noteApp'>
    <div ng-controller='mainController'>
        <table class='table table-striped table-bordered table-hover'>
            <thead>
                <tr>
                    <th scope="col">Id</th>
                    <th scope="col">Title</th>
                    <th scope="col">Description</th>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="board in boards">
                    <td></td>
                    <td></td>
                    <td></td>
                </tr>
            </tbody>
        </table>
        <div>
            <label>
                Student Name:
                <input type="text" ng-model='firstname'>
            </label>
            <p ng-cloak>My name is </p>

            <!-- Directives: ng-if -->
            <!-- ng-if removes element from DOM when condition is false -->
            <!-- Length of Firstname must be greater than $scope.minCharacters -->
            <div class="alert alert-info" ng-if="firstname.length <= minCharacters">
                ng-if: Firstname must be longer than  characters
            </div>

            <!-- Directives: ng-show -->
            <!-- ng-show sets display:none when condition becomes false -->
            <div class="alert alert-info" ng-show="firstname.length <= minCharacters">
                ng-show: Firstname is too short, must be more than  characters
            </div>

            <!-- Directives: ng-show -->
            <!-- ng-show sets display:none when condition becomes false -->
            <div class="alert alert-info" ng-hide="firstname.length > minCharacters">
                ng-hide: Firstname is too short, must be more than  characters
            </div>

            <!-- Directives: ng-class -->
            <!-- Set class alert-success if firstname greater than minCharacters -->
            <div class="alert"ng-show="firstname.length !== minCharacters" ng-class="{'alert-success': firstname.length > minCharacters, 'alert-warning': firstname < minCharacters}">
                <div ng-show="firstname.length <= minCharacters">
                    ng-class: First name is too short, must be  characters long
                </div>
                <div ng-show="firstname.length > minCharacters">
                    ng-class: Firstname is valid
                </div>
            </div>

            <!-- Directives: ng-click -->
            <!-- Set a click handler -->
            <div>
                <label>ng-click Directive</label>
                <button ng-click="clickedMe()">Click me!</button>
            </div>

            <!-- using $http.post() to post data to API endpoint -->
            Add Student: <input type="text" ng-model="newStudent" />
            <a href="#" class="btn btn-secondary" ng-click="addStudent()">Save</a>

            <!-- // Custom directive called search-result -->
            <!-- // Different ways of calling custom directive -->
            <div search-results></div>
            <div class='search-results'></div>
            <search-results> * this text will be transcluded </search-results>

            <!-- // repeat items in an array of objects -->
            <div search-results ng-repeat="student in students"></div>

        </div>
    </div>
</body>
</html>