AngularJS Hello World

Table of Contents

1 Before We Begin

1.1 Requirements

Our hello world project has the following requirements:

Note: Node.js is only used for running Karma and Jasmine.

1.2 Install and Set Up

1.2.1 Project Structure

AngularJS doesn't impose any particular project structure on you.

For this tutorial, the following project structure is used:

  • lib/
  • images/
  • js/
    • helloworld/
  • styles/
  • tests/

JavaScript source code is in js/, the CSS is in styles/ and any images go into images/. Test code is in tests/. The lib/ directory is where JavaScript and CSS libraries are placed so that we don't get confused between our code and external code that others have written.

1.2.2 Install AngularJS

Download AngularJS and place it in the lib/ directory.

<script src="lib/angular-1.0.8/angular.js"></script>
<script src="js/helloworld/app.js"></script>
<script src="js/helloworld/controllers.js"></script>
<link rel="stylesheet" href="styles/helloworld.css" />

1.2.3 Install the Karma test runner and Jasmine test framework

We create a package.json so that we can list the dependencies of our project which lets us use npm to automatically install them.

{
  "name": "learning-angularjs",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "karma": "0.10.2",
    "karma-firefox-launcher": "0.1.0",
    "karma-jasmine": "0.1.3"
  }
}

Karma is a test runner and it uses the Jasmine test framework. Karma lets us run unit tests and end to end tests. The Jasmine test framework gives us functions for unit testing.

1.2.4 Setting up Karma for Unit Testing

Before we can get to unit testing the controller, we have to create configuration file for Karma.

We want to set Karma to use Jasmine as our test framework:

'frameworks': [
  'jasmine'
]

And we also want to be able to run the tests within the Firefox and Chrome browsers if they're available:

'browsers': [
  'Chrome',
  'Firefox'
]

We want Karma to watch all of our library, source and unit-test files and to reload them when they're updated. The order of the files is important and determines the order they're loaded in. You will want the AngularJS library to load first, followed by the source code, and then finally the unit-test files.

'files': [
  // Angular files
  'lib/angular-1.0.8/angular.js',
  'lib/angular-1.0.8/angular-*.js',

  // Hello World source code
  'js/helloworld/app.js',
  'js/helloworld/controllers.js',

  // Hello World test files
  'tests/*.js'
]

We also need to exclude the angular scenario source file. It's used for end-to-end/integration testing and isn't needed for our unit tests:

'exclude': [
  'lib/angular-1.0.8/angular-scenario.js'
]

Here's how it looks put together:

module.exports = function (config) {
  config.set({
    'frameworks': [
      'jasmine'
    ],
    'browsers': [
      'Chrome',
      'Firefox'
    ],
    'files': [
      // Angular files
      'lib/angular-1.0.8/angular.js',
      'lib/angular-1.0.8/angular-*.js',

      // Hello World source code
      'js/helloworld/app.js',
      'js/helloworld/controllers.js',

      // Hello World test files
      'tests/*.js'
    ],
    'exclude': [
      'lib/angular-1.0.8/angular-scenario.js'
    ]
  });
};

1.2.5 Running the Karma Unit Tests

You can run the unit tests like this:

./node_modules/karma/bin/karma start karma.conf.js --auto-watch

If Karma is installed globally, you can run it like this:

karma start karma.conf.js --auto-watch

The --auto-watch option will run tests as soon as any source files have changed. You can add autoWatch: true to the karma configuration setup so that you don't have to add --auto-watch on the command-line.

2 Learning AngularJS

2.1 Concepts

2.1.1 Directives

Directives are specialized reusuable HTML elements. They contain code for manipulating the DOM and for adding events.

  • Example: ng-repeat
    For example, the ng-repeat directive will repeat whatever child elements it contains.
    <div ng-repeat="person in people">
      <h2>{{ person.fullName }}</h2>
      <p>{{ person.firstName }} is {{ person.age }} years old.</p>
      <p>Address: {{ person.address }}</p>
    </div>
    

    Expands into:

    <div ng-repeat="person in people">
      <h2>Alice Example</h2>
      <p>Alice is 30 years old.</p>
      <p>Address: 1 Some St., Boston, MA, United States of America</p>
      <h2>Bob Sample</h2>
      <p>Bob is 32 years old.</p>
      <p>Address: 876 Another St., Toronto, ON, Canada</p>
    </div>
    
  • Example: ng-click
    Another example; the ng-click directive won't modify the HTML but it will bind a function to the click event on the element.
    <button ng-click="alert('hello world')">Click me</button>
    

    Expands into the same HTML but when you click, an alert will pop up that says "hello world":

    <button ng-click="alert('hello world')">Click me</button>
    

2.1.2 Controllers

A controller is an object that controls what data is displayed in the app and how the user can interact with the app. A controller can fetch data from a server and put it into its scope so that the data can be displayed. When a user clicks on a button, the controller contains the function that will be executed on that click.

2.1.3 Expressions

Angular expressions is code within within curly braces. You can put math expressions in them, refer to variables within the controller's scope, and run variables through filters among other things.

2.1.4 Filters

Filters are functions for reformatting variables or other data that is passed to them. The most commonly used filters are for formatting how a date is displayed and for displaying a decimal number as a currency.

  • Example: currency
    {{ '25.09' | currency }}
    

    After filtering it turns into this:

    $25.09
    

2.2 Defining the Hello World module

To begin with, we must create a module that represents the whole app. Creating a module helps us avoid polluting the global namespace.

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

2.3 Using the Hello World module

After defining the app's module, we can use it on our site by specifiying it as the value for the ng-app attribute in the top-level html element:

<html ng-app="helloworldApp">

This will give us access to any controllers, directives, services, filters and other objects that are part of the helloworldApp.

Note: There can only be one ng-app declared on a page.

2.4 Defining a Controller

We're going to define our own controller, the HelloworldCtrl, which contains a list of books. Each book has a title and a price that we're going to display. We're going to be able to sort the books by their title and price and we're also going to be able to search through the book titles.

this.books = [
  {
    'title': 'Mastering Web Application Development with AngularJS',
    'price': '35.99'
  },
  {
    'title': 'AngularJS for Dummies',
    'price': '10.95'
  },
  {
    'title': 'Learn AngularJS in 24 Hours',
    'price': '29.00'
  },
  {
    'title': 'Learn AngularJS in 24 Hours, 2nd Edition',
    'price': '9.95'
  }
];

var books = this.books;
$scope.books = books;

Typically, this data would be fetched from a server using a REST API. In this tutorial, we're simplifying things and providing all the data to the controller that we want to work with.

To make unit testing easier, we add the mock data as a property of the controller object. A reference to the mock data is stored so that there are no binding issues when using the this keyword (A List Apart has a good article on this, "Getting Out of Binding Situations in JavaScript").

2.4.1 Sorting and Searching the Books

The list of books can be sorted by the title or the price. Books that include booksTitleContains in their title will be visible, we're going to use an empty string or null to specify whether to search the books by title or to display all books in the list.

$scope.booksSortedBy = 'title-ascending';
$scope.booksTitleContains = '';

Then we have to sort and search the list of books so that we know what to display:

var searchAndSortBooks = function () {
  var i;
  var searchTitleRegExp;
  var result = [];

  // Searching for titles containing the search string
  if ($scope.booksTitleContains && $scope.booksTitleContains != '') {
    searchTitleRegExp = new RegExp($scope.booksTitleContains, 'i');
    for (i = 0; i < books.length; i++) {
      if (searchTitleRegExp.test(books[i].title)) {
        result.push(books[i]);
      }
    }
  } else {
    result = books;
  }

  // Sorting the books
  if ($scope.booksSortedBy.match(/title/)) {
    $scope.books.sort(function (a, b) {
      if (a.title < b.title) {
        return -1;
      } else if (a.title > b.title) {
        return 1;
      }
      return 0;
    });
  } else if ($scope.booksSortedBy.match(/price/)) {
    $scope.books.sort(function (a, b) {
      if (parseFloat(a.price) < parseFloat(b.price)) {
        return -1;
      } else if (parseFloat(a.price) > parseFloat(b.price)) {
        return 1;
      }
      return 0;
    });
  }

  if ($scope.booksSortedBy.match(/descending/)) {
    $scope.books.reverse();
  }

  $scope.books = result;
};

searchAndSortBooks();

When either the booksSortedBy or booksTitleContains scope variables change, we trigger the function and update the list of books that are displayed. To do that, we use the $watch method in the $scope object. It will watch for changes in the given expression and execute whatever callback function we provide.

$scope.$watch(
  'booksSortedBy + "," + booksTitleContains',
  function (newValue, oldValue) {
    searchAndSortBooks();
  }
);

Initially, the expression that is watched will look like this:

title-ascending,

When we start entering a title to search for, the expression being watched will change:

title-ascending,simple title

When we change how we sort the books, the expression again will change:

price-descending,my favourite book

Whenever that expression changes, the callback function that we provided will be executed.

2.4.2 Putting it all together

We create the controller as part of the helloworldApp module. We specify the $scope as a dependency and then we pass in a function that defines the controller.

angular.module('helloworldApp').controller(
  'HelloworldCtrl',   // name of the controller
  [
    '$scope',         // DI (Dependency Injection) modules
    function ($scope) { // definition of the controller
      this.books = [
        {
          'title': 'Mastering Web Application Development with AngularJS',
          'price': '35.99'
        },
        {
          'title': 'AngularJS for Dummies',
          'price': '10.95'
        },
        {
          'title': 'Learn AngularJS in 24 Hours',
          'price': '29.00'
        },
        {
          'title': 'Learn AngularJS in 24 Hours, 2nd Edition',
          'price': '9.95'
        }
      ];

      var books = this.books;
      $scope.books = books;
      $scope.booksSortedBy = 'title-ascending';
      $scope.booksTitleContains = '';
      var searchAndSortBooks = function () {
        var i;
        var searchTitleRegExp;
        var result = [];

        // Searching for titles containing the search string
        if ($scope.booksTitleContains && $scope.booksTitleContains != '') {
          searchTitleRegExp = new RegExp($scope.booksTitleContains, 'i');
          for (i = 0; i < books.length; i++) {
            if (searchTitleRegExp.test(books[i].title)) {
              result.push(books[i]);
            }
          }
        } else {
          result = books;
        }

        // Sorting the books
        if ($scope.booksSortedBy.match(/title/)) {
          $scope.books.sort(function (a, b) {
            if (a.title < b.title) {
              return -1;
            } else if (a.title > b.title) {
              return 1;
            }
            return 0;
          });
        } else if ($scope.booksSortedBy.match(/price/)) {
          $scope.books.sort(function (a, b) {
            if (parseFloat(a.price) < parseFloat(b.price)) {
              return -1;
            } else if (parseFloat(a.price) > parseFloat(b.price)) {
              return 1;
            }
            return 0;
          });
        }

        if ($scope.booksSortedBy.match(/descending/)) {
          $scope.books.reverse();
        }

        $scope.books = result;
      };

      searchAndSortBooks();
      $scope.$watch(
        'booksSortedBy + "," + booksTitleContains',
        function (newValue, oldValue) {
          searchAndSortBooks();
        }
      );
    }
  ]
);

2.5 Using the Controller

Once we have defined the controller, we can use it in our app. The HelloworldCtrl is our top-level controller.

<div ng-controller="HelloworldCtrl">

2.5.1 Displaying the books

We're going to display the list of books in a table using the Angular's ng-repeat directive which will loop through each book. We can use the ng-bind directive to display the book's title or price, or we can use an Angular expression. We'll use both, ng-bind for the title and an expression for the price. The price will be formatted using the currency filter.

<table>
  <thead>
    <th>Title</th>
    <th>Price</th>
  </thead>
  <tbody>
    <tr ng-repeat="book in books">
      <td ng-bind="book.title"></td>
      <td>{{ book.price | currency }}</td>
    </tr>
  </tbody>
</table>

2.5.2 Sorting the books

To sort the books, there will be a drop down menu. It contains all possible sorting options. When one of these options is selected, the variable booksSortedBy in the HelloworldCtrl controller's scope will be updated to whatever the value of the option is.

<p>Sort by:
  <select ng-model="booksSortedBy">
    <option value="title-ascending">Title: Ascending</option>
    <option value="title-descending">Title: Descending</option>
    <option value="price-ascending">Price: Low to High</option>
    <option value="price-descending">Price: High to Low</option>
  </select>
</p>

When you select "Price: High to Low", the value of $scope.booksSortedBy is "price-descending".

2.5.3 Searching the books

When we enter text into the search box, the booksTitleContains variable in the scope will be set to whatever value we entered. Since we're watching the value, the search will automatically be executed.

The clear button makes it faster to stop the search and to display all books.

<p>
  <input type="text" ng-model="booksTitleContains"></input>
  <button ng-click="booksTitleContains = null">Clear</button>
</p>

2.5.4 Putting it all together

Putting it all together we get:

<<helloworld-controller-div-html>>
  <h1>Hello World!</h1>
  <h2>Books</h2>
  <<helloworld-controller-books-search-html>>
  <<helloworld-controller-books-sort-html>>
  <<helloworld-controller-books-list-html>>
</div>

2.6 Unit Testing the Controller

There are a few moving parts in our controller; we can sort the list of books, and we can search for them by title. We need to test both of these to make sure that they work and we can do this manually, by loading up the page and clicking on and off different buttons and visually confirming that the list of books is correct.

This can quickly become tedious when you start adding many more controllers. So we want to automate the testing by writing some unit tests.

2.6.1 Setup Before Each Test

Before each test we want to setup the controller:

var scope;
var ctrl;

beforeEach(module('helloworldApp'));

beforeEach(inject(function ($rootScope, $controller) {
  scope = $rootScope.$new();
  ctrl = $controller('HelloworldCtrl', { $scope: scope });
}));

The module and inject functions are defined at the global level and within the angular object. The module function gets the module prepared for testing. The inject function injects dependencies which provide the functionality required, it allows mock objects to be used when testing.

The $rootScope dependency provides the function to define a new scope for the controller. The $controller dependency provides a way to look up and create an instance of a controller.

2.6.2 Test to Make Sure the Controller Exists

Here's how we test to make sure that the controller, HelloworldCtrl, exists in the module, helloworldApp:

it('should have a HelloworldCtrl controller', function () {
  expect(ctrl).not.toBe(null);
});

2.6.3 Tests for Book Sorting

Here's how we test the book sorting.

it('sorts books by title in ascending order', function () {
  var i;
  scope.booksSortedBy = 'title-ascending';
  scope.$apply();
  for (i = 0; i < scope.books.length - 1; i++) {
    expect(scope.books[i].title).toBeLessThan(scope.books[i + 1].title);
  }
});

it('sorts books by title in descending order', function () {
  var i;
  scope.booksSortedBy = 'title-descending';
  scope.$apply();
  for (i = 0; i < scope.books.length - 1; i++) {
    expect(scope.books[i].title).toBeGreaterThan(scope.books[i + 1].title);
  }
});

it('sorts books by price in ascending order', function () {
  var i;
  scope.booksSortedBy = 'price-ascending';
  scope.$apply();
  for (i = 0; i < scope.books.length - 1; i++) {
    expect(parseFloat(scope.books[i].price)).toBeLessThan(parseFloat(scope.books[i + 1].price));
  }
});

it('sorts books by price in descending order', function () {
  var i;
  scope.booksSortedBy = 'price-descending';
  scope.$apply();
  for (i = 0; i < scope.books.length - 1; i++) {
    expect(parseFloat(scope.books[i].price)).toBeGreaterThan(parseFloat(scope.books[i + 1].price));
  }
});

Note that after changing a scope variable, we have to call the $apply method. This is part of the Angular life cycle and it handles exceptions and then executes any $watch expressions that we have setup.

2.6.4 Tests for Book Searching

Here's how we test book searching:

it('lists all books when searching for null or an empty string', function () {
  scope.booksTitleContains = '';
  scope.$apply();
  expect(scope.books.length).toBe(ctrl.books.length);

  scope.booksTitleContains = null;
  scope.$apply();
  expect(scope.books.length).toBe(ctrl.books.length);
});

it('lists only books that contain the search string in their title', function () {
  scope.booksTitleContains = 'Web';
  scope.$apply();
  expect(scope.books.length).toBe(1);
  expect(scope.books[0].title).toMatch('Web');

  scope.booksTitleContains = 'AngularJS';
  scope.$apply();
  expect(scope.books.length).toBe(ctrl.books.length);
  expect(scope.books[1].title).toMatch('AngularJS');

  scope.booksTitleContains = 'Learn AngularJS';
  scope.$apply();
  expect(scope.books.length).toBe(2);
  expect(scope.books[0].title).toMatch('Learn AngularJS');
});

Note that again, after changing the $scope variable, we have to call $apply to go through the Angular life cycle.

2.6.5 Putting it all together

describe('Hello World Controller', function () {
  var scope;
  var ctrl;

  beforeEach(module('helloworldApp'));

  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();
    ctrl = $controller('HelloworldCtrl', { $scope: scope });
  }));

  it('should have a HelloworldCtrl controller', function () {
    expect(ctrl).not.toBe(null);
  });

  it('sorts books by title in ascending order', function () {
    var i;
    scope.booksSortedBy = 'title-ascending';
    scope.$apply();
    for (i = 0; i < scope.books.length - 1; i++) {
      expect(scope.books[i].title).toBeLessThan(scope.books[i + 1].title);
    }
  });

  it('sorts books by title in descending order', function () {
    var i;
    scope.booksSortedBy = 'title-descending';
    scope.$apply();
    for (i = 0; i < scope.books.length - 1; i++) {
      expect(scope.books[i].title).toBeGreaterThan(scope.books[i + 1].title);
    }
  });

  it('sorts books by price in ascending order', function () {
    var i;
    scope.booksSortedBy = 'price-ascending';
    scope.$apply();
    for (i = 0; i < scope.books.length - 1; i++) {
      expect(parseFloat(scope.books[i].price)).toBeLessThan(parseFloat(scope.books[i + 1].price));
    }
  });

  it('sorts books by price in descending order', function () {
    var i;
    scope.booksSortedBy = 'price-descending';
    scope.$apply();
    for (i = 0; i < scope.books.length - 1; i++) {
      expect(parseFloat(scope.books[i].price)).toBeGreaterThan(parseFloat(scope.books[i + 1].price));
    }
  });

  it('lists all books when searching for null or an empty string', function () {
    scope.booksTitleContains = '';
    scope.$apply();
    expect(scope.books.length).toBe(ctrl.books.length);

    scope.booksTitleContains = null;
    scope.$apply();
    expect(scope.books.length).toBe(ctrl.books.length);
  });

  it('lists only books that contain the search string in their title', function () {
    scope.booksTitleContains = 'Web';
    scope.$apply();
    expect(scope.books.length).toBe(1);
    expect(scope.books[0].title).toMatch('Web');

    scope.booksTitleContains = 'AngularJS';
    scope.$apply();
    expect(scope.books.length).toBe(ctrl.books.length);
    expect(scope.books[1].title).toMatch('AngularJS');

    scope.booksTitleContains = 'Learn AngularJS';
    scope.$apply();
    expect(scope.books.length).toBe(2);
    expect(scope.books[0].title).toMatch('Learn AngularJS');
  });
});

2.7 Defining a Directive

2.8 Unit Testing the Directive

2.9 End to End Testing

3 Putting it all together

<!DOCTYPE HTML>
<html ng-app="helloworldApp">
<head>
  <title>Hello Angular.JS</title>
  <link rel="stylesheet" href="styles/helloworld.css" />
  <script src="lib/angular-1.0.8/angular.js"></script>
  <script src="js/helloworld/app.js"></script>
  <script src="js/helloworld/controllers.js"></script>
</head>
<body>
  <div ng-controller="HelloworldCtrl">
    <h1>Hello World!</h1>
    <h2>Books</h2>
    <p>
      <input type="text" ng-model="booksTitleContains"></input>
      <button ng-click="booksTitleContains = null">Clear</button>
    </p>
    <p>Sort by:
      <select ng-model="booksSortedBy">
        <option value="title-ascending">Title: Ascending</option>
        <option value="title-descending">Title: Descending</option>
        <option value="price-ascending">Price: Low to High</option>
        <option value="price-descending">Price: High to Low</option>
      </select>
    </p>
    <table>
      <thead>
        <th>Title</th>
        <th>Price</th>
      </thead>
      <tbody>
        <tr ng-repeat="book in books">
          <td ng-bind="book.title"></td>
          <td>{{ book.price | currency }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</body>
</html>

Date: 2013-10-09 17:31:13 EDT

Author: Rudolf Olah

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0