AngularJS Official Tutorial (Part 1): explained step by step

1. Prerequisites

The latest version of AngularJS is 1.8.3

If you are using Angular Framework higher versions you should first uninstall it from your computer

npm uninstall -g @angular/cli

image

Now we install the AngularJS in our laptop. Run this command

npm install -g angular@1.8.2

image

2. Step 0: Bootstraping

Initialize the project directory

mkdir angularjs-app
cd sample2_basic

We open VSCode with this command

code .

Then we initialize/create the package.json file ruuning this command (OPTIONAL)

npm init -y

We install angular dependencies with this command

npm install angular

We have an optinal way for installing the dependencies: bootstrap.css and angular.js files

We can create manually the folders structure (with mkdir) and then download the libraries with curl command

mkdir -p lib/bootstrap/dist/css
cd lib/bootstrap/dist/css
curl -O https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.css
mkdir -p lib/angular
cd lib/angular
curl -O https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.js

We create the folders and files project structure

image

We copy the following source code

app/index.html

<!doctype html>
<html lang="en" ng-app>
  <head>
    <meta charset="utf-8">
    <title>My HTML File</title>
    <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
    <script src="lib/angular/angular.js"></script>
  </head>
  <body>
    <p>Nothing here {{'yet' + '!'}}</p>
  </body>
</html>

This is the code architecture explanation

image

To run the applicaton we right click on the index.html file and select the menu option Open with Live Server

image

This is the output we see

image

3. Step 1: Static Template

We will create a purely static HTML page and then examine how we can turn this HTML code into a template that AngularJS will use to dynamically display the same result with any set of data

<html lang="en" ng-app>
  <head>
    <meta charset="utf-8">
    <title>Google Phone Gallery</title>
    <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="app.css" />
    <script src="lib/angular/angular.js"></script>
  </head>
  <body>

    <ul>
      <li>
        <span>Nexus S</span>
        <p>
          Fast just got faster with Nexus S.
        </p>
      </li>
      <li>
        <span>Motorola XOOM™ with Wi-Fi</span>
        <p>
          The Next, Next Generation tablet.
        </p>
      </li>
    </ul>

  </body>
</html>
body {
    padding-top: 20px;
  }

This is the project folders and files structure

image

Now we can run the application and see the output

image

4. Step 2: AngularJS Template

Now, it's time to make the web page dynamic — with AngularJS

We will also add a test that verifies the code for the controller we are going to add

For AngularJS applications, we encourage the use of the Model-View-Controller (MVC) design pattern to decouple the code and separate concerns

With that in mind, let's use a little AngularJS and JavaScript to add models, views, and controllers to our app

The list of three phones is now generated dynamically from data

This is the application source code

index.html

<html lang="en" ng-app="phonecatApp">
  <head>
    <meta charset="utf-8">
    <title>Google Phone Gallery</title>
    <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <script src="lib/angular/angular.min.js"></script>
    <script src="app.js"></script>
  </head>
  <body ng-controller="PhoneListController">

    <ul>
      <li ng-repeat="phone in phones">
        <span>{{phone.name}}</span>
        <p>{{phone.snippet}}</p>
      </li>
    </ul>

  </body>
</html>

app.css

body {
    padding-top: 20px;
  }

app.js

'use strict';

// Define the `phonecatApp` module
var phonecatApp = angular.module('phonecatApp', []);

// Define the `PhoneListController` controller on the `phonecatApp` module
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
  $scope.phones = [
    {
      name: 'Nexus S',
      snippet: 'Fast just got faster with Nexus S.'
    }, {
      name: 'Motorola XOOM™ with Wi-Fi',
      snippet: 'The Next, Next Generation tablet.'
    }, {
      name: 'MOTOROLA XOOM™',
      snippet: 'The Next, Next Generation tablet.'
    }
  ];
});

app.spec.js

'use strict';

describe('PhoneListController', function() {

  beforeEach(module('phonecatApp'));

  it('should create a `phones` model with 3 phones', inject(function($controller) {
    var scope = {};
    var ctrl = $controller('PhoneListController', {$scope: scope});

    expect(scope.phones.length).toBe(3);
  }));

});

This is the application architecture

image

How to run the application

We first have to define the project dependencies in the package.json file

package.json

{
  "name": "angularjs_official_sample_step4",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "bootstrap": "3.3.x"
  },
  "devDependencies": {
    "angular": "^1.8.3",
    "angular-mocks": "^1.8.3",
    "jasmine-core": "^5.1.2",
    "karma": "^6.4.3",
    "karma-chrome-launcher": "^3.2.0",
    "karma-jasmine": "^5.1.0",
    "karma-ng-html2js-preprocessor": "^1.0.0"
  }
}

To install the above dependencies we execute the command

npm i

or

npm install

The file package-lock.json file will be created

Also the libraries will be installed in node_modules

This is the project folders and files structure

image

To run the application we right click on the index.html file and selecting the Open with Live Server menu option

image

This is the application output

image

How to run the application tests

Install Karma CLI: Install Karma's command-line interface globally

npm install -g karma-cli

Then install the required dependencies:

npm install karma karma-jasmine jasmine-core karma-chrome-launcher karma-ng-html2js-preprocessor --save-dev

Create a Karma Configuration File:

karma init karma.conf.js

During the setup, answer the prompts as follows:

Testing framework: Jasmine

Use Require.js: No

Capture browsers: Chrome (you can add more browsers later)

Test result reporter: progress (or choose as per your preference)

Other questions: default options should be fine

Modify the karma.conf.js file to include your application and test files

   // list of files / patterns to load in the browser
    files: [
      'node_modules/angular/angular.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'app/*.js',
      'app/*.spec.js'
    ],

To Run the Tests execute this command:

karma start karma.conf.js

This is the output we get

image

image

5. Step 3: Components

In the previous step, we saw how a controller and a template worked together to convert a static HTML page into a dynamic view

Since this combination (template + controller) is such a common and recurring pattern, AngularJS provides an easy and concise way to combine them together into reusable and isolated entities, known as components

AngularJS will create a so called isolate scope for each instance of our component

To create a component, we use the .component() method of an AngularJS module

This is the project folders and files structure

image

This is the applicaiton source code

index.html

<!DOCTYPE html>
<html lang="en" ng-app="phonecatApp">
<head>
  <meta charset="utf-8">
  <title>Google Phone Gallery</title>
  <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css" />
  <link rel="stylesheet" href="app.css" />
  <script src="../node_modules/angular/angular.min.js"></script>
  <script src="app.js"></script>
  <script src="phone-list.component.js"></script>
</head>
<body>

  <!-- Use a custom component to render a list of phones -->
  <phone-list></phone-list>

</body>
</html>

app.js

'use strict';

angular.module('phonecatApp', []);

app.css

body {
    padding-top: 20px;
  }

phone-list.component.js

'use strict';

// Register `phoneList` component, along with its associated controller and template
angular.
  module('phonecatApp').
  component('phoneList', {
    template:
        '<ul>' +
          '<li ng-repeat="phone in $ctrl.phones">' +
            '<span>{{phone.name}}</span>' +
            '<p>{{phone.snippet}}</p>' +
          '</li>' +
        '</ul>',
    controller: function PhoneListController() {
      this.phones = [
        {
          name: 'Nexus S',
          snippet: 'Fast just got faster with Nexus S.'
        }, {
          name: 'Motorola XOOM™ with Wi-Fi',
          snippet: 'The Next, Next Generation tablet.'
        }, {
          name: 'MOTOROLA XOOM™',
          snippet: 'The Next, Next Generation tablet.'
        }
      ];
    }
  });

phone-list.component.spec.js

'use strict';

describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phonecatApp'));

  // Test the controller
  describe('PhoneListController', function() {

    it('should create a `phones` model with 3 phones', inject(function($componentController) {
      var ctrl = $componentController('phoneList');

      expect(ctrl.phones.length).toBe(3);
    }));

  });

});

This is the application arthitecture

image

We run the application and this is the output

image

image

We run the Tests and this is the output

image

6. Step 4: Directory and File Organization

We are going to refactor our codebase and move files and code around, in order to make our application more easily expandable and maintainable

  • Put each entity in its own file

  • Organize our code by feature area, instead of by function

  • Split our code into modules that other modules can depend on

This is the project folders and files structure

image

This is the application source code:

app/index.html

<!DOCTYPE html>
<html lang="en" ng-app="phonecatApp">
<head>
  <meta charset="utf-8">
  <title>Google Phone Gallery</title>
  <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css" />
  <link rel="stylesheet" href="app.css" />
  <script src="../node_modules/angular/angular.min.js"></script>
  <script src="app.module.js"></script>
  <script src="phone-list/phone-list.module.js"></script>
  <script src="phone-list/phone-list.component.js"></script>
</head>
<body>

  <!-- Use a custom component to render a list of phones -->
  <phone-list></phone-list>

</body>
</html>

app/app.module.js

'use strict';

// Define the `phonecatApp` module
angular.module('phonecatApp', [
  // ...which depends on the `phoneList` module
  'phoneList'
]);

app/app.css

body {
    padding-top: 20px;
  }

app/phone-list/phone-list.component.js

'use strict';

// Register `phoneList` component, along with its associated controller and template
angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: function PhoneListController() {
      this.phones = [
        {
          name: 'Nexus S',
          snippet: 'Fast just got faster with Nexus S.'
        }, {
          name: 'Motorola XOOM™ with Wi-Fi',
          snippet: 'The Next, Next Generation tablet.'
        }, {
          name: 'MOTOROLA XOOM™',
          snippet: 'The Next, Next Generation tablet.'
        }
      ];
    }
  });

app/phone-list/phone-list.component.spec.js

'use strict';

describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phoneList'));

  // Test the controller
  describe('PhoneListController', function() {

    it('should create a `phones` model with 3 phones', inject(function($componentController) {
      var ctrl = $componentController('phoneList');

      expect(ctrl.phones.length).toBe(3);
    }));

  });

});

app/phone-list/phone-list.module.js

'use strict';

// Define the `phoneList` module
angular.module('phoneList', []);

app/phone-list/phone-list.template.js

<ul>
    <li ng-repeat="phone in $ctrl.phones">
      <span>{{phone.name}}</span>
      <p>{{phone.snippet}}</p>
    </li>
</ul>

How to run the application

image

image

How to run the Tests

Modify the karma.conf.js file

...
    // list of files / patterns to load in the browser
    files: [
      'node_modules/angular/angular.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'app/phone-list/phone-list.module.js',
      'app/app.module.js',
      'app/phone-list/*.js', // to include all JS files in the phone-list folder
      'app/**/*spec.js'
    ],
...

This is the test output

image

7. Step 5: Filtering repeaters

We will add full-text search. The phone list on the page changes depending on what a user types into the search box

We will also write an end-to-end (E2E) test

the data that a user types into the input box (bound to $ctrl.query) is immediately available as a filter input in the list repeater (phone in $ctrl.phones | filter:$ctrl.query)

This is the application source code:

phone-list.template.html

<div class="container-fluid">
  <div class="row">
    <div class="col-md-2">
      <!--Sidebar content-->

      Search: <input ng-model="$ctrl.query" />

    </div>
    <div class="col-md-10">
      <!--Body content-->

      <ul class="phones">
        <li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
          <span>{{phone.name}}</span>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>

    </div>
  </div>
</div>

Source code explanation

Container Fluid (<div class="container-fluid">):

This div is a Bootstrap class that creates a full-width container

It ensures that the content inside it is responsive and spans the full width of the viewport

Row (<div class="row">):

This div creates a horizontal group of columns using Bootstrap's grid system

Sidebar Column (<div class="col-md-2">):

This div defines a column that takes up 2 out of 12 parts of the row (using Bootstrap's grid system for medium and larger devices)

Search Input (<input ng-model="$ctrl.query" />):

This is an input field bound to the AngularJS model $ctrl.query. The ng-model directive binds the input value to the controller's query property, enabling two-way data binding.

Main Content Column (<div class="col-md-10">):

This div defines a column that takes up the remaining 10 out of 12 parts of the row

Phone List (<ul class="phones">):

This unordered list will display a list of phones

Repeating List Items (<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">):

The ng-repeat directive is used to iterate over each phone in the $ctrl.phones array

The filter:$ctrl.query part filters the phones based on the query input. It only displays phones that match the query

Phone Name and Snippet (<span>{{phone.name}}</span>, <p>{{phone.snippet}}</p>):

{{phone.name}} and {{phone.snippet}} are AngularJS expressions that display the phone's name and snippet, respectively

Functionality

The search input allows the user to type a query

As the user types, the ng-model directive updates the $ctrl.query property

The ng-repeat directive dynamically filters the list of phones based on the query

Only phones whose properties match the query are displayed in the list

This is the application architecture

image

How to run the application

This is the application output

image

image

How to run the end-to-end (e2e) test for your AngularJS application using Protractor

  1. Install Protractor: Make sure you have Protractor installed. If not, you can install it globally using npm
npm install -g protractor
  1. Download your Chromedriver

Navigate to this web page and download the latest chromedriver version

https://googlechromelabs.github.io/chrome-for-testing/#stable

We select the stable version link

image

Then we download the windows 64 version

image

Unzip the file and copy in a folder

image

Set the PATH environmental variable

image

Run the Task Manager and delete chromedriver.exe proccess

image

image

Also delete the chromedriver.exe file from the path:

.../AppData/Roaming/npm/node_modules/protractor/node_modules/webdriver_manager/selenium/chromedriver_114.0.57

  1. Update WebDriver: Protractor uses WebDriver to interact with your web application. Make sure you update WebDriver to get the latest version
webdriver-manager update
  1. Start WebDriver: Before running your tests, you need to start the WebDriver server
webdriver-manager start

Open one Terminal Window in VSCode and run the above command

image

  1. Protractor Configuration File: Ensure you have a Protractor configuration file (protractor.conf.js)

This file tells Protractor where to find your tests and how to run them. Here’s an example configuration file:

protractor.conf.js

exports.config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    specs: ['e2e-tests/*.js'], // Adjust the path to your test files as needed
    capabilities: {
      'browserName': 'chrome'
    },
    framework: 'jasmine',
    jasmineNodeOpts: {
      defaultTimeoutInterval: 30000
    },
    onPrepare: function() {
      browser.ignoreSynchronization = true;  // Disable synchronization globally
    },
    chromeDriver: 'C:/chromedriver/chromedriver.exe'  // Specify the path to the new ChromeDriver
  };

e2e-tests/scenarios.js

'use strict';

describe('PhoneCat Application', function() {

    describe('phoneList', function() {
  
      beforeEach(function() {
        browser.ignoreSynchronization = true;  // Disable Angular synchronization
        browser.get('http://127.0.0.1:5500/app/index.html');
      });
  
      it('should filter the phone list as a user types into the search box', function() {
        var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
        var query = element(by.model('$ctrl.query'));
  
        expect(phoneList.count()).toBe(3);
  
        query.sendKeys('nexus');
        expect(phoneList.count()).toBe(1);
  
        query.clear();
        query.sendKeys('motorola');
        expect(phoneList.count()).toBe(2);
      });
  
    });

});
  1. Run the Test: In VSCode, you can run your e2e test using the integrated terminal

Open a second Terminal Window in VSCode and run this command

protractor protractor.conf.js

image

8. Step 6: Two-way Data Binding

We will add a feature to let our users control the order of the items in the phone list

The dynamic ordering is implemented by creating a new model property, wiring it together with the repeater, and letting the data binding magic do the rest of the work

The application now displays a drop-down menu that allows users to control the order in which the phones are listed

This is the application source code

app/phone-list/phone-list.component.js

'use strict';
// Register `phoneList` component, along with its associated controller and template
angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: function PhoneListController() {
      this.phones = [
        {
          name: 'Nexus S',
          snippet: 'Fast just got faster with Nexus S.',
          age: 1
        }, {
          name: 'Motorola XOOM™ with Wi-Fi',
          snippet: 'The Next, Next Generation tablet.',
          age: 2
        }, {
          name: 'MOTOROLA XOOM™',
          snippet: 'The Next, Next Generation tablet.',
          age: 3
        }
      ];

      this.orderProp = 'age';
    }
  });

app/phone-list/phone-list.component.spec.js

'use strict';

describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phoneList'));

  // Test the controller
  describe('PhoneListController', function() {

    it('should create a `phones` model with 3 phones', inject(function($componentController) {
      var ctrl = $componentController('phoneList');

      expect(ctrl.phones.length).toBe(3);
    }));

  });

});

app/phone-list/phone-list.template.html

<div class="col-md-2">
  <!--Sidebar content-->

  <p>
    Search:
    <input ng-model="$ctrl.query" />
  </p>

  <p>
    Sort by:
    <select ng-model="$ctrl.orderProp">
      <option value="name">Alphabetical</option>
      <option value="age">Newest</option>
    </select>
  </p>

</div>
<div class="col-md-10">
  <!--Body content-->

  <ul class="phones">
    <li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
      <span>{{phone.name}}</span>
      <p>{{phone.snippet}}</p>
    </li>
  </ul>
</div>
</div>
</div>

e2e-tests/scenarios.js

'use strict';
// AngularJS E2E Testing Guide:
// https://docs.angularjs.org/guide/e2e-testing
describe('PhoneCat Application', function() {
  describe('phoneList', function() {
    beforeEach(function() {
      browser.get('index.html');
    });
    it('should filter the phone list as a user types into the search box', function() {
      var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
      var query = element(by.model('$ctrl.query'));
      expect(phoneList.count()).toBe(3);
      query.sendKeys('nexus');
      expect(phoneList.count()).toBe(1);
      query.clear();
      query.sendKeys('motorola');
      expect(phoneList.count()).toBe(2);
    });

    it('should be possible to control phone order via the drop-down menu', function() {
      var queryField = element(by.model('$ctrl.query'));
      var orderSelect = element(by.model('$ctrl.orderProp'));
      var nameOption = orderSelect.element(by.css('option[value="name"]'));
      var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));

      function getNames() {
        return phoneNameColumn.map(function(elem) {
          return elem.getText();
        });
      }

      queryField.sendKeys('tablet');   // Let's narrow the dataset to make the assertions shorter

      expect(getNames()).toEqual([
        'Motorola XOOM\u2122 with Wi-Fi',
        'MOTOROLA XOOM\u2122'
      ]);

      nameOption.click();

      expect(getNames()).toEqual([
        'MOTOROLA XOOM\u2122',
        'Motorola XOOM\u2122 with Wi-Fi'
      ]);
    });

  });

});

This is the application architecture

image

How to run the application

image

image

image