AngularJS UI Development

4.7 (3 reviews total)
By Amit Gharat , Matthias Nehlsen
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Setting Up the Environment

About this book

AngularJS and its rich set of components solve many of the problems developers face when writing reliable single page applications in ways that would not be possible using other frameworks. This book will help you expand your horizons by teaching you the skills needed to successfully design, customize, build, and deliver real-world applications in AngularJS. We will start off by setting up a fully automated environment to quickly scaffold, test, and deploy any application. Along the way, we'll cover how to design and build production-ready applications to demonstrate how innovative and powerful AngularJS is. By leveraging CSS3 animations, we'll convert them into intuitive and native-like applications in no time. You will also learn how to use Grunt for application-specific task management, Bower to manage dependencies with external libraries/plugins, Git for better versioning, and Karma and Protractor for automated testing to build applications the way experts do.

You will learn all this by building real-world applications including a to-do application, Github dashboard, project management application, and many more.

Publication date:
October 2014
Publisher
Packt
Pages
258
ISBN
9781783288472

 

Chapter 1. Setting Up the Environment

In this chapter, we will set up the environment to work with AngularJS. We will do a bit of this manually first to understand the concepts before using more automated approaches. By the end of the chapter, you will have a decent understanding of the AngularJS tool chain without having to rely on the magic of scaffolding tools, such as Yeoman (http://yeoman.io). There is nothing wrong whatsoever with using tools such as Yeoman later on, but there will be times when a deeper understanding serves you well.

We will follow the tradition of building a Hello World application. It would be rather boring to only have this application say hello to the world; instead, let's build an application in which we can enter a name to be greeted with. Let's see the two-way data binding in action by automatically updating another element on the page when the name changes.

Then, we will also wrap the markup for this example in a very basic directive that we will develop together. I have the impression that somehow writing custom directives is seen as a very advanced topic. Well, so be it; it will be all the more gratifying to get a basic introduction over with in this initial chapter.

Once this is done, we will build the scaffolding around this Hello World application by giving it a proper and meaningful structure that will allow us to use the result as a template for subsequent chapters. This scaffolding will already contain a test or two plus the means to build the application (bundling JavaScript in one file, for example).

You can download the source for this chapter; however, I cannot recommend it highly enough to actually go through the effort of typing these few lines of code. This always really helps me retain things, which I don't find all that surprising. After all, reading and then typing engages more parts of the brain than just reading something. However, ultimately it is, of course, up to you.

Even if you do choose to download the source code, please do yourself a favor and play around with it. Change things, break things, and fix them; all this will enable you to learn more in a shorter span of time.

In particular, in this chapter, we will:

  • Build a very basic Hello World application

  • Refine this application by using an object as the model and an additional input field

  • Create our first directive

  • Install Node.js and NPM (we'll need these for the subsequent tasks)

  • Install and use Bower

  • Write unit tests and run them using Karma

  • Write integration tests using Protractor

  • Create a build system with Grunt.js

  • Manage client-side dependencies with Bower

  • Learn how to use Git as a version control system

So, without further ado, let's get started.

 

Hello World


I assume you have used AngularJS before. If not, you may want to go through the tutorials at http://angularjs.org to get a better understanding of the framework, particularly the famed two-way data binding. You might get along well by just following the examples in this chapter too.

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you. The code samples for this title are also hosted on GitHub at https://github.com/matthiasn/AngularUI-Code. The GitHub code is maintained and updated, and is the author's preferred repository.

Let's start with the markup; the simplest HTML5 plus some basic CSS for the styling will suffice for now. Create a directory anywhere you want, create an src subdirectory, and put a file named index.html with the following content in it. We will use the input field later to allow the user to input a name:

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body>
    <div>Hello World!</div>
    <input></input>
  </body>
</html>

Then, create a subdirectory named css with a file named main.css and with the following content:

body {
    font-family: sans-serif;
    font-size: 1.2em;
    margin: 50px;
}

Okay, this will do for the moment. Now, let's see how we can make this interactive by adding some AngularJS code. First, you need to download AngularJS, which you can find at https://angularjs.org/. Download the zip archive; we will need a few files from this. The latest version at the time of writing this book was v1.2.21.

Note

You can also use a Content Delivery Network (CDN) instead to serve the angular.js files. Google, for example, hosts the AngularJS files; check out this link:

https://developers.google.com/speed/libraries/devguide#angularjs

Using a CDN can be advantageous when the file is already in the cache of the user's browser, as it will then reduce the load time for the application.

Now, create a js subdirectory; create a vendor subdirectory inside the js directory, in which you place the angular.js file.

Then, create two empty files named app.js and controllers.js inside the js subdirectory. The directory structure of our application now looks as follows:

Tip

At this point, it does not matter whether you use the minified version of AngularJS or not. In production, it is highly recommended that you use the minified version (of any library) though, as this will reduce the page load, particularly on slow mobile connections.

Then, add the following to app.js:

'use strict';
angular.module('myApp', ['myApp.controllers']);

The preceding code creates an AngularJS module named myApp and requires the myApp.controllers module, which we will create in the next step by adding the following code to controllers.js:

'use strict';
angular.module('myApp.controllers', []).
    controller('helloWorldCtrl', function ($scope) {
        $scope.name = "World";
    });

The preceding code creates the myApp.controllers module with the helloWorldCtrl controller. This controller holds the $scope object, which is the (admittedly simple) model we are creating for the application. The name variable on the $scope object will be used for the two-way data binding that AngularJS is famous for.

With the files in the right places, let's edit the index.html file so that AngularJS can teach our browser some two-way data binding tricks:

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body data-ng-app="myApp">
    <div data-ng-controller="helloWorldCtrl">
      <div>Hello {{ name }}!</div>
      <input data-ng-model="name"></input>
    </div>
    <script src="js/vendor/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
  </body>
</html>

Note

If you want to use a CDN, consider this line:

<script src="js/vendor/angular.js"></script>

Replace it with the following code:

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular.min.js"></script>

This is it for the first version of our Hello World example. Open index.html in the browser and you can see two-way data binding in action, as follows:

You can change the name in the input field, and the text in the header will update automatically and immediately, as follows:

Using objects instead of primitives

Let's change one more thing that will save us from trouble later. Data models should reference objects and not primitives, such as a string, because the latter can create a headache when used in directives. Directives have a child $scope object that receives the model property by reference in the case of an object but as a copy when a primitive value is used, in which case, we might introduce difficult-to-spot errors.

Tip

You might ask what this $scope object is. The $scope object is an object that holds the data for the current environment, such as inside a controller or inside a directive. Now, you can pass a property of the controller's $scope object into the directive, which then becomes a property of the directive's own $scope object. For this, it is important to understand how JavaScript deals with parameter passing. When you pass an object to a function, the object is passed by reference, meaning that wherever this passed object is modified, these modifications reach back the sender. When passing objects to directives, this is exactly the behavior we want. For example, we have a directive to modify personal details, and we pass a person object from the controller to this directive. When we now edit the details, we expect the change to reach the controller. However, when we pass primitives such as Strings or Numbers, they are copied instead of being passed by reference. When we modify these in the directive, the changes will not be reflected in the controller's $scope object.

In controllers.js, we only need to change one line, introducing an object with properties for the first and last names:

'use strict';
angular.module('myApp.controllers', []).
    controller('helloWorldCtrl',function ($scope) {
        $scope.name = { first: "Jane", last: "Doe" };
    });

In index.html, let's create an additional field so that each can be edited separately:

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body data-ng-app="myApp">
    <div data-ng-controller="helloWorldCtrl">
      <h1>Hello {{ name.first }} {{ name.last }}!</h1>
      <input data-ng-model="name.first"></input>
      <input data-ng-model="name.last"></input>
    </div>
    <script src="js/vendor/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
  </body>
</html>

Building our first directive

With these changes, we are well prepared to create a basic custom directive.

Tip

A directive is a way to create a custom element with its own behavior, which we can then use later as a building block in our application. One example of such a directive could be a login form. In this case, we will just create a directive for greeting us, with the div for both the greeting and input fields. Directives can be of arbitrary complexity, and we can use other directives inside the markup of a directive that we define.

For this, add a directives.js file to the js folder, with the following content:

'use strict';
angular.module('myApp.directives', [])
.directive('helloWorld', function () {
    return {
        restrict: 'AE',
        scope: { name: "=name" },
        template: 
"<h1>Hello {{ name.first }} {{ name.last }}!</h1>" +
   "<input data-ng-model='name.first'></input>" + 
  "<input data-ng-model='name.last'></input>"
        }
    });

AngularJS will now place the markup in the previous template property, inside the element that uses this directive. This might not seem terribly useful yet, but it will be when elements are repeated, for example.

Tip

Note that we can also load the markup from a template file, which is much more convenient than the previous multiline string, particularly as the directive gets more complex. However, for this simple example, this should serve us well.

The index.html file now becomes simpler than before:

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body data-ng-app="myApp">
    <div data-ng-controller="helloWorldCtrl">
      <div hello-world name="name"></div>
    </div>
    <script src="js/vendor/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
    <script src="js/directives.js"></script>
  </body>
</html>

Tip

Older versions of Chrome do not support the loading of the template when the index.html file is loaded from the filesystem; instead, Chrome complains of cross-origin requests. In order to quickly set up a web server, you can use SimpleHTTPServer from Python, which comes with OS X and Linux. All you need for this is to run the following command in the command line inside the application folder:

python -m SimpleHTTPServer 8000

Of course, you can use any other server to serve your folder, but this is really simple and often useful.

Nice! We just built our first directive. Much more can be done with them; directives are a really powerful concept for encapsulating UI elements. However, it's a start.

Note

Directives can apply to elements, classes, and attributes. In this case, we chose an attribute named hello-world, as follows:

      <div hello-world name="name"></div>

We could use an element, shown as follows; try it out by adding this line below the previous line:

      <hello-world name="name"></hello-world>

All we need to change now is this line in directives.js:

      restrict: 'AE',

Here, A means attribute, E stands for element, and C stands for class. Check out step 3a in the accompanying source code to see both versions side by side.

However, using custom elements might not work in older browsers. You have been warned!

 

Installing Node.js and NPM


We will need to install a couple of additional tools to test and build our application. The foundation for this will be Node.js and NPM, the node package manager. Node.js is a platform built on top of V8, Google's JavaScript runtime, which is also used in Chrome. You can also build server-side, networked applications with Node.js, but here, it will only power our test and build the system. For now, think of it as a way to run the JavaScript code outside the browser. NPM is the package manager for Node.js, which makes it quite simple to install additional functionality. It comes bundled with Node.js.

The easiest way to install Node.js is to follow the instructions at http://nodejs.org.

OS X

On a Mac, you could either run the aforementioned installer or you could use Homebrew, a package manager for many open source libraries.

More information on Homebrew can be found at http://brew.sh/. Run the following command in the Terminal after installing Homebrew:

# brew install npm

The previous command will automatically install Node.js as a dependency of NPM if necessary.

Shell commands will run in a Terminal window. If you haven't used the Terminal before, go to the magnifying glass icon in the upper-right corner of your screen and type Terminal. You will need to open a new window after the installation is complete in order to run Node or NPM from a Terminal window.

Windows

On Windows, the easiest way to install Node.js is to run the Windows installer from http://nodejs.org. This installer, by default, now also installs NPM.

Command-line commands will run in the Windows command prompt. On Windows 7, this is available by clicking on the home button and entering cmd in the search box. You will need to open a new command-line window after the installation in order to be able to run Node or NPM from the command line.

Linux (Ubuntu)

On Ubuntu, there are two ways of installing Node and NPM, as follows:

  1. Preferred: download the source package from the Node.js web page and build it yourself (this may take a few minutes with a lot of output). This way, you are guaranteed to have the latest version. For this, perform the following steps:

    1. Uncompress the package.

    2. Open the folder in the Terminal.

    3. Inside the directory, run the following commands:

      	# ./configure
      	# make
      	# sudo make install  
      
    4. You may have to install G++ first:

      	# sudo apt-get install g++
      
  2. You can also install NPM through Ubuntu's package manager by running the following command in the Terminal:

    # sudo apt-get install npm
    

    This will install Node.js as a dependency of NPM as well. Afterwards, if you want to upgrade NPM itself, then run the following command:

    # sudo npm install –g npm
    

    While this approach works, you may get an outdated version; so, the first approach is generally recommended.

 

Managing client-side dependencies with Bower


Earlier in this chapter, we downloaded AngularJS and placed the angular.js file in our directory structure manually. This was not so bad for a single file, but this process will very soon become tedious and error-prone when dealing with multiple dependencies.

Luckily, there is a great tool for managing client-side dependencies, and it can be found at http://bower.io.

Bower allows us to record which dependencies we need for an application. Then, after downloading the application for the first time, we can simply run bower install and it will download all the libraries and assets specified in the configuration file for us.

First, we will need to install Bower (potentially with sudo):

# npm install -g bower

Now, let's try it out by running the following command:

# bower install angular

Tip

You will notice an error message when running the previous command on a machine that does not have Git installed. In this case, please head to the final part of this chapter and follow the installation instructions.

Okay, this will download AngularJS and place the files in the app/bower_components/ folder. However, our remaining sources are in the src/ folder, so let's store the Bower files here as well. Create a file named .bowerrc in the project root, with these three lines in it:

{
  "directory": "src/bower"
}

This will tell Bower that we want the managed dependencies inside the src/bower/ folder. Now, remove the app/ folder and run the earlier bower install command one more time. You should see the AngularJS files in the src/bower/ folder now.

Now, we said we wanted to record the dependencies in a configuration file so that we can later run bower install after downloading/checking out the application.

Tip

Why can't we just store all the dependencies in our version control system? Of course, we can, but this would bloat the repository a lot. Instead, it is better to focus on the artifacts that we created ourselves and pull in the dependencies when we check out the application for the first time.

We will create the configuration file now. We could do this by hand or let Bower do it for us. Let's do the latter and then examine the results:

# bower init

This will start a dialog that guides us through the initial Bower project setup process. Give the application a name, version number, and description as you desire. The main file stays empty for now. In the module type selection, just press Enter on the first option. Whenever you see something in square brackets, for example, [MIT], this is the default value that will be used when you simply press Enter. When we confirm that the currently installed dependencies should be set as dependencies, AngularJS will automatically appear as a project dependency in the configuration file if you have followed the previous instructions. Finally, let's set the package as private. There is no need to have it accidentally appear in the Bower registry. Once we are done, the bower.json file should look roughly as follows:

{
  name: 'Hello World',
  version: '0.0.0',
  homepage: 'https://github.com/matthiasn/AngularUI-Code',
  authors: ['Matthias Nehlsen <[email protected]>'],
  description: 'Fancy starter application',
  license: 'MIT',
  private: true,
  ignore: [
    '**/.*',
    'node_modules',
    'bower_components',
    'src/bower',
    'test',
    'tests'
  ],
  dependencies: {
    angular: '~1.2.22'
  }
}

Now that we have a dependency management in place, we can use AngularJS from here and delete the manually downloaded version inside the js/vendor/ folder. Also, edit index.html to use the file from the bower/ directory. Find the following line of code:

    <script src="js/vendor/angular.js"></script>

Replace it with this:

    <script src="bower/angular/angular.js"></script>

Now, we have a package manager for client-side dependencies in place. We will use more of this in the next step.

 

Testing the Hello World application


We might be tempted to leave the application as it is. It works as expected; we can convince ourselves of this easily by changing the name and then observing if the rendering changes as expected. However, this is not a viable approach for more complex applications. In those scenarios, we can easily break something and then overlook the problem, only to be reminded by customers that something is not working as expected. This can always happen, even in an application with decent test coverage. However, we can dramatically decrease the chance of being called at night because something broke by implementing a proper test strategy, consisting of both unit and integration tests.

Unit tests

Let's start with unit tests for the controller and the directive we just built. Unit tests, as the name suggests, test the individual units of an application, as opposed to integration tests. We will take a look at those later.

We will need additional files and directories for the tests that we are about to create. Our test runner script and some of the directory structure are inspired by the angular-seed project (https://github.com/angular/angular-seed).

Create additional files and directories so that your project structure resembles the following. Note that the src/bower directory contains additional files and folders, but you can ignore this as these are managed by Bower:

We will need angular-mocks.js to run the tests. Now that we have Bower in place, adding angular-mocks to our project becomes very simple; just run this single command:

# bower install --save angular-mocks

The preceding command will not only download the dependency, but thanks to the --save option, it will also add it to the bower.json file as a project dependency.

Installing Karma and Jasmine

Karma is a test runner provided by the AngularJS team. More on Karma can be found at http://karma-runner.github.io/0.12/index.html.

Karma allows us to run all the tests that we write against our codebase so that we can see at a glance when we break something. These tests can be run either once or on a continuous basis so that they always run again when a file changes.

The actual tests will be written with the help of Jasmine, a JavaScript testing library. More on Jasmine can be found at http://jasmine.github.io.

We will see tests written with Jasmine soon; however, we first need to install Karma and karma-jasmine plus the browser-specific test launchers. From inside your project directory, run the following commands:

# npm install karma 
# npm install karma-jasmine
# npm install karma-chrome-launcher
# npm install karma-firefox-launcher

This will install Karma and karma–jasmine plus the Firefox and Chrome launchers locally, inside the node_modules subdirectory. The launchers are just examples; there are more for all the relevant browsers on the market. For the demonstration now, let's use these two.

We could run Karma from inside a nested subdirectory; however, it is not very convenient; it would be better to always have the command available anywhere. We can achieve this by installing karma-cli as follows:

# npm install –g karma-cli

Note that the -g option installs Node modules globally. The exact location of where these modules are installed depends on your platform and configuration. You might have to run the previous command with superuser privileges, as follows, depending on your local configuration:

# sudo npm install -g karma-cli

Only use sudo when the command fails.

With these tools in place, let's edit the karma.conf.js file so that we can run the unit tests in both Firefox and Chrome:

module.exports = function(config){
    config.set({
    basePath : '../',
    files : [
      'src/bower/angular/angular.js',
      'src/bower/angular-mocks/angular-mocks.js',
      'src/js/**/*.js',
      'test/unit/**/*.js'
    ],
    autoWatch: true,
    frameworks: ['jasmine'],
    browsers : ['Chrome', 'Firefox'],
    plugins : ['karma-chrome-launcher', 
               'karma-firefox-launcher', 
               'karma-jasmine']
})};

We specify which files to load and which framework and browsers to use when running the tests. Note that files and subdirectories are matched using asterisks. Then, we specify that autoWatch is true, meaning that Karma will watch for filesystem changes and rerun whenever a file has changed. Thus, we get a very tight feedback loop. When we break something, we will notice it immediately.

We also specify that we want to use Jasmine as the testing framework. Other frameworks can also be used here; however, Jasmine is a good choice and the one we will be using for this book. Just note that you could use others here. Finally, we specify the plugins, which have the same names as the NPM modules we loaded earlier. Here, you could, for example, delete the Firefox launcher if you only wanted to run the tests in Chrome or vice versa.

Now, we will edit the test.sh file:

#!/bin/bash
BASE_DIR=`dirname $0`
echo ""
echo "Starting Karma Server (http://karma-runner.github.io)"
echo "-------------------------------------------------------"
karma start $BASE_DIR/../conf/karma.conf.js $*

Now all that remains to make the test script runnable is the following command in the Terminal inside our main folder, chmod +x scripts/test.sh (this applies to both Mac OS X and Linux). On Windows, you create a test.bat batch file instead:

@echo off
set BASE_DIR=%~dp0
karma start "%BASE_DIR%\..\conf\karma.conf.js" %*

Let's now create the test for the controller by adding the following to the test/unit/controllers.spec.js file:

'use strict';
describe('controller specs', function() {
  var $scope;

  beforeEach(module('myApp.controllers'));
 
  beforeEach(inject(function($rootScope, $controller) {
    $scope = $rootScope.$new();
    $controller('helloWorldCtrl', {$scope: $scope});
  }));
 
  it('should create "name" model with first name "Jane"', function() {
    expect($scope.name.first).toBe("Jane");
  });
});

The test is contained in the describe block, in which we create a fake $scope object and then pass this $scope object to an instance of the helloWorldCtrl controller. As the it-block suggests, we expect the $scope object to now have a name model that contains an object with the first property that is equal to the string Jane. If this condition is true, the test passes.

Next, we want to create a simple test for the hello-world directive. Add the following to the test/unit/directives.spec.js file:

'use strict';
describe('specs for directives', function() {
  beforeEach(module('myApp.directives'));
  var $scope; 
  beforeEach(inject(function($rootScope) {
    $scope = $rootScope.$new();    
     $scope.name = {first: "John", last: "Doe"};
  }));

  describe('hello-world', function() {
    it('should contain the provided name', function() {
      inject(function($compile) {
        var element = $compile('<div hello-world name="name"></div>')($scope);
         $scope.$digest();
        expect(element.html()).toContain("John");
      });
    });
  });
});

The previous test creates a $scope object, this time with a name model whose first property equals John. We compile an HTML snippet that contains the directive inside a <div> tag, passing the mock $scope object. After calling $scope.$digest(), we would expect the compiled element to contain the string John, and running the test as described later will show that this expectation is indeed warranted.

Now, you can run the test by typing the following in the Terminal inside our application folder. This is how the output looks on OS X:

$ scripts/test.sh
Starting Karma Server (http://karma-runner.github.io)
--------------------------------------------------------
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [launcher]: Starting browser Firefox
INFO [Firefox 24.0.0 (Mac OS X 10.8)]: Connected on socket […]
INFO [Chrome 30.0.1599 (Mac OS X 10.8.5)]: Connected on socket […]
Firefox 24.0.0 (Mac OS X 10.8): Executed 2 of 2 SUCCESS (0.142 secs / 0.025 secs)
Chrome 30.0.1599 (Mac OS X 10.8.5): Executed 2 of 2 SUCCESS (0.541 secs / 0.036 secs)
TOTAL: 4 SUCCESS

On Ubuntu, the output looks very similar. The following is the output when the test is configured to only run Firefox:

$ scripts/test.sh 
Starting Karma Server (http://karma-runner.github.io)
--------------------------------------------------------
INFO [karma]: Karma v0.12.21 server started at http://localhost:9876/
INFO [launcher]: Starting browser Firefox
INFO [Firefox 31.0.0 (Ubuntu)]: Connected on socket […]
Firefox 31.0.0 (Ubuntu): Executed 2 of 2 SUCCESS (0.054 secs / 0.041 secs)

This concludes the unit testing for our simple example. In larger applications, the test scenarios will, of course, be much more complex and potentially complicated, but the principles remain the same.

Integration / end-to-end tests with Protractor

Unit testing is a great start towards feeling more secure that our application will work even after changing parts of the code base. However, unit testing does not cover the entirety of the application. Let's take our simple application that we have written so far as an example. What else can we test beyond looking at individual parts?

We could test interactions with the application and then observe if it reacted the way we expected. In this very simple example, this could be done by opening the application in the browser, changing the first and last names, and then seeing whether the displayed greeting changed accordingly.

So far, we don't need tools for this; we can simply do this by hand. However, it gets increasingly tedious as the application grows, especially when we want to test this interaction in multiple browsers.

Luckily, there is a tool for this called Protractor, which is also built by the AngularJS team. You can find out more about Protractor at https://github.com/angular/protractor.

Let's install and configure Protractor according to the tutorial on its GitHub page, and apply the tutorial to our own code. We start with the installation of the module, which will also install webdriver-manager as a dependency, and then update it:

# npm install –g protractor
# webdriver-manager update

Once again, you might have to run the previous commands with superuser (sudo) privileges. Once this is complete, we can start the server with the following command:

# webdriver-manager start

Tip

Note that you will need a Java runtime on your system in order to run webdriver. You can obtain this from http://java.com/en/download/index.jsp.

On a Mac, you don't have to worry about this. If you don't have a JVM (Java Virtual Machine) installed yet, the system will notice this and ask you whether it should download it for you.

On Ubuntu, you can also run the following command instead of using the previous installer:

# sudo apt-get install default-jre

This will start a server application that controls different browsers. We can now write test scenarios that will call this server application, which will in turn start browsers as specified, run the tests, and allow us to observe if the results are as desired.

We will write such a test soon, but first, we need to get another prerequisite installed: a simple local web server that serves our project over HTTP: http-server.

This is an NPM module that gives us a little tool that we can then call on the command line and that will then serve the content of the working directory over HTTP, on a specified port. We install it as follows (potentially using sudo):

# npm install -g http-server

Let's try it out right away by starting the following from inside our project directory:

# http-server -a localhost -p 8000

The previous command will start the server on port 8000 and only listen on the localhost interface. You could leave out the –a localhost option, but this might irritate your firewall. Once this is running, you can open the following URL in the browser:

http://localhost:8000/src/

If you have started http-server from the root of the project, you should now see our application loaded and working.

Now, let's write a simple test only to check the title of the page, just to check that the tool chain is working. For this, create a file named protractor.conf.js inside the conf folder with the following content:

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['../test/protractor/spec.js'], 
  capabilities: {
      browserName: 'firefox'
  }
}

The previous configuration reads as follows:

  • The address of our selenium instance (the webdriver) is the same that the server displays when we started it about a page ago, http://localhost:4444/wd/hub.

  • We will run a single test file for now, spec.js, in the test/protractor directory of the project root. We will create this one next.

  • We will run the test in only one browser for now; let's use Firefox this time. Here, you could, for example, use Chrome instead or select one of the many other options. We could also use multiple browsers for each and every test. The Protractor documentation has all the required information.

Now, create spec.js inside the test/protractor folder as follows:

 describe(hello world app', function() {
  it('should have a title', function() {
    browser.get('http://localhost:8000/src/');
    expect(browser.getTitle()).toEqual('Hello World');
  });
});

The previous test will open a browser, Firefox in the case of the current configuration; open the page at http://localhost:8000/src/ and check if the title of the page is Hello World, as expected, let's run it:

# protractor conf/protractor.conf.js

We should see the following result:

Using the selenium server at http://localhost:4444/wd/hub
Finished in 0.315 seconds
1 test, 1 assertion, 0 failures

Now, change the title of the page or the expected string in the test, run the test again, and you will see that the output changes:

Using the selenium server at http://localhost:4444/wd/hub
Failures:
  1) angularjs homepage should have a title
   Message:
     Expected 'Hello World' to equal 'Hello Worlds'.
   Stacktrace: [...]
Finished in 0.307 seconds
1 test, 1 assertion, 1 failure

Great, we can not only see that something went wrong but also exactly which expectation failed.

Now, the previous test alone is not all that useful; let's add some interaction by adding the following to the end of spec.js:

describe('name fields', function() {
    it('should be filled out and editable', function() {
        browser.get('http://localhost:8000/src/');
        
        var h1 = element.all(by.css('h1')).first();
        var fname = element.all(by.tagName('input')).first();
        var lname = element.all(by.tagName('input')).get(1);
        expect(h1.getText()).toEqual("Hello Jane Doe!");
        expect(fname.getAttribute('value')).toEqual("Jane");
        expect(lname.getAttribute('value')).toEqual("Doe");
        
        fname.clear().sendKeys('John');
        lname.clear().sendKeys('Smith');
        
        expect(h1.getText()).toEqual("Hello John Smith!");
        expect(fname.getAttribute('value')).toEqual("John");
        expect(lname.getAttribute('value')).toEqual("Smith");
  });
});

Let's go through the previous additional test:

  • We open a browser window.

  • We create variables for the elements because we will use each variable multiple times. Note that there are multiple ways to select elements, for example, here we are using both by.css and by.tagName.

  • We expect the elements in their initial state to have the correct values.

  • We clear the input elements and enter text through the sendKeys method.

  • We expect the elements to have the correct values after the model has changed through our text input.

The previous section has really only scratched the surface of what we can do with the very powerful Protractor. Please refer to the useful and detailed documentation and tutorial to learn more. It will be time well spent.

 

Building the application


Preparing an application for production environments can be a tedious task. For example, it is highly recommended that you create a single file that contains all the JavaScript needed by the application instead of multiple small files, as this can dramatically reduce page load time, particularly on slow mobile connections with high latency. It is not feasible to do this by hand though, and it is definitely no fun.

This is where a build system really shines. We are going to use Grunt for this. A single command in the Terminal will run the tests and then put the necessary files (with a single, larger JavaScript file) for the application in the dist/ folder. Many more tasks can be automated with Grunt, such as minifying the JavaScript, automating tasks by watching folders, running JsHint (http://www.jshint.com), but we can only cover a fairly basic setup here.

Let's get started. We need to install Grunt first (possibly with sudo):

# npm install –g grunt-cli

Then, we create a file named package.json in the root of our application folder with the following content:

{
  "name": "my-hello-world",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-concat": "~0.5.0",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-targethtml": "~0.2.6",
    "grunt-karma": "~0.8.3",
    "karma-jasmine": "~0.1.5",
    "karma-firefox-launcher": "~0.1.3",
    "karma-chrome-launcher": "~0.1.4"
  }
}

This file defines the NPM modules that our application depends on. These will be installed automatically by running npm install inside the root of our application folder.

Next, we define which tasks Grunt should automate by creating Gruntfile.js, also in the root of our application folder, with the following content:

module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/js/vendor/*.js','src/js/*.js'],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    },
    copy: {
      main: {
        src: 'src/css/main.css',
        dest: 'dist/css/main.css',
      },
    },
    targethtml: {
      dist: {
        files: {
          'dist/index.html': 'src/index.html'
        }
      }
    },
    karma: {
      unit: {
        configFile: 'conf/karma.conf.js',
        singleRun: true
      }
    }
  });
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-copy');
  grunt.loadNpmTasks('grunt-targethtml');
  grunt.loadNpmTasks('grunt-karma');
  grunt.registerTask('dist', ['karma', 'concat', 'targethtml', 'copy']);
};

This file contains different sections that are of interest. In the first part, the configuration object is created, which defines the options for the individual modules. For example, the concat section defines that a semicolon should be used as a separator when it merges all JavaScript files into a single file inside the dist/js folder, with the name of the application. It is important that angular.js comes before the application code inside this file, which is guaranteed by the order inside the src array. Files in the vendor subfolder are processed first with this order.

The copy task configuration is straightforward; here, we only copy a single CSS file into the dist/css folder. We will do more interesting things with CSS later when talking about CSS frameworks.

The targethtml task processes the HTML so that it only loads the one concatenated JavaScript file. For this to work, we need to modify index.html, as follows:

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Angular UI Template</title>
    <link rel="stylesheet" href="css/main.css">
  </head>
  <body data-ng-app="myApp">
    <div data-ng-controller="helloWorldCtrl">
      <div hello-world name="name"></div>
    </div>
    <!--(if target dev)><!-->
    <script src="js/vendor/angular.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
    <script src="js/directives.js"></script>    
    <!--<!(endif)-->
    <!--(if target dist)><!-->
    <script src="js/my-hello-world.js"></script>
    <!--<!(endif)-->
  </body>
</html>

This, together with the configuration, tells the targethtml task to only leave the section for the dist task inside the HTML file, effectively removing the section that will load individual files.

Note

You might be tempted to think that it will not make much of a difference if one or multiple files need to be retrieved when the page is loaded. After all, the simple concatenation step does not reduce the overall size of what needs to be loaded.

However, particularly on mobile networks, it makes a huge difference because of the latency of the network. When it takes 300 ms to get a single response, it soon becomes noticeable whether one or ten files need to be loaded. This is still true even when you get the maximum speed in 3G networks. LTE significantly reduces latency, so the difference is not quite as noticeable. The improvements with LTE only occur in ideal conditions, so it is best not to count on them.

The karma section does nothing more than tell the karma task where to find the previously defined configuration file and that we want a single test run for now. Next, we tell Grunt to load the modules for which we have created the configuration, and then we define the dist task, consisting of all the previously described tasks.

All that remains to be done is to run grunt dist in the command line when we want to test and build the application. The complete AngularJS web application can then be found in the dist/ folder.

Running Protractor from Grunt

Let's also run Protractor from our grunt task. First, we need to install it as follows:

# npm install grunt-protractor-runner --save-dev

This will not only install the grunt-protractor-runner module, but also add it as a dependency to package.json so that when you, for example, check out your application from your version control system (covered next) on a new computer and you want to install all your project's dependencies, you can simply run:

# npm install

Tip

If you follow along using the companion source code instead of typing the source code yourself, you will need to run npm install again in the last step's folder.

Next, edit Gruntfile.js so that from the karma section, it looks as follows:

    karma: {
      unit: {
        configFile: 'conf/karma.conf.js',
        singleRun: true
      }
    },
    protractor: {
      e2e: {
        options: {
          configFile: 'conf/protractor.conf.js'
        } 
      }
    }
  });
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-copy');
  grunt.loadNpmTasks('grunt-targethtml');
  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-protractor-runner');
  grunt.registerTask('dist', ['karma', 'protractor', 'concat', 'targethtml', 'copy']);
};

Now, Protractor will also run every single time we call grunt dist to build our application. The build process will be stopped when either the karma or the protractor step reports an error, keeping us from ever finding code in the dist folder when fails tests.

Note

Note that we will need to have both the webdriver-manager and http-server modules running in separate windows for the grunt dist task to work. As a little refresher, these were started as follows:

# webdriver-manager start
# http-server –a localhost –p 8000

Both can also be managed by Grunt, but that makes the configuration more complex and it would also mean that the task runs longer because of startup times. These can also be part of a complex configuration that watches folders and runs and spawns all the required tasks automatically. Explore the Grunt documentation further to tailor an environment specific to your exact needs.

We will expand on our basic usage of Grunt and do more sophisticated tasks later, for example, to create a single, minified CSS file using a CSS framework or to apply minification to the concatenated JavaScript file when further optimizing for mobile web applications in Chapter 10, Mobile Development Using AngularJS and Bootstrap. Now that you know how the build system works in general, you may already want to explore more advanced features. The project's website is a good place to start (http://gruntjs.com).

 

Managing the source code with Git


You are probably using Git already. If not, you really should. You need to have it installed for working with Bower anyway, so why not use it for your own projects?. Git is a distributed Version Control System (VCS). I cannot imagine working without it, neither in a team nor when working on a project myself. We don't have the space to cover Git for team development here in either case; this topic can easily fill an entire book. However, if you are working in a team that uses Git, you will probably know how to use it already. What we can do in this introductory chapter is go through the basics for a single developer.

First, you need to install Git if you do not have it on your system yet.

Note

OS X

On a Mac, again the easiest way to do this is using Homebrew (http://brew.sh). Run the following command in the Terminal after installing Homebrew:

# brew install git

Windows

On Windows, the easiest way to install Git is to run the Windows installer from http://git-scm.com/downloads.

Linux (Ubuntu)

On Ubuntu, run the following command in the shell:

# sudo apt-get install git

Let's initialize a fresh repository in the current directory:

git init

Then, we create a hidden file named .gitignore, for now with only the following content:

node_modules

Note

This tells Git to ignore the hundreds of files and directories in the node_modules folder. These don't need to be stored in our version control system because the modules can be restored by running npm install in the root folder of the application instead, as all the dependencies are defined in the package.json file.

Next, we add all files in the current directory (and in all subdirectories):

git add 

Next, we freeze the file system in its current state:

git commit –m "initial commit"

Now, we can edit any file, try things out, and accidentally break things without having to worry because we can always come back to anything that was committed into the VCS. This adds incredible peace of mind when you are playing around with the code.

When you issue the git status command, you will notice that we are on a branch called master. A project can have multiple branches at the same time; these are really useful when working on a new feature. We should always keep master as the latest stable version; additional features (or bug fixes) should always be worked upon in a separate branch. Let's create a new feature branch called additional-feature:

git branch additional-feature

Once it is created, we need to check out the branch:

git checkout additional-feature

Now, when the new code is ready to be committed, the process is the same as before:

git add .
git commit –m "additional feature added"

We should commit early and often; this habit will make it much easier to undo previous changes when things go wrong. Now, when everything is working in the new branch (all the tests pass), we can go back into the master branch:

git checkout master

Then, we can merge the changes back into the master branch:

git merge additional-feature

Being able to freely change between branches, for example, makes it very easy to go back to the master branch from whatever you are working on and do a quick bug fix (in a specialized branch, ideally) without having to think about what you just broke with the current changes in the new feature branch. Please don't forget to commit before you switch the branch though. You can merge the bug fix in this example back into the master, go back to the feature branch you were working on, and even pull those changes that were just done in the master branch into the branch you are working on. For this, when you are inside the feature branch, merge the changes:

git merge master

If these changes were in different areas of your files, this should run without any difficulties. If they were in the same lines, you will need to manually resolve the conflicts.

Tip

Using Git is a really useful way to manage source code. However, it is in no way limited to code files (or text files, for that matter). Any file can be placed under source control. For example, this book was written with heavy usage of Git, for any file involved. This is extremely useful when you are trying to go back to the previous versions of any file.

 

Summary


We started this chapter by covering some basics about AngularJS. Then, we built a very simple Hello World app with two-way data binding. We then upgraded the simple application to use an object for the property of the model instead of using string primitives.

Afterwards, we built our first custom directive and moved parts of the markup of our application into this custom directive.

Then, we covered testing. We learned why testing is essential for more complex applications. For testing, we first installed Karma with Jasmine to run the first unit tests. We also created our first integration tests, which are useful for testing how the parts of the application play together.

We then installed and configured Grunt.js as a build system for our application. A properly configured build system frees us from having to run tedious and potentially error-prone tasks over and over again.

Finally, we briefly took a look at the usage of Git, an open source distributed version control system, which is optional for the rest of the book but highly encouraged.

In the next chapter, we will begin with AngularUI, a suite of tools and building blocks, for making application development with AngularJS simpler and more productive.

About the Authors

  • Amit Gharat

    Amit Gharat is a full-stack engineer and open source contributor. He has built and made some of his personal projects open source, such as Directives, SPAs, and Chrome extensions written in AngularJS. He has an excessive urge to share his programming experiences in an easy-to-understand language through his personal blog in order to inspire and help others. When not programming, he enjoys reading, watching videos on YouTube, and watching comedy shows with his family. He has also written an article for Appliness and Sdjournal Magazine, Poland.

    Browse publications by this author
  • Matthias Nehlsen

    Matthias Nehlsen is a freelance software engineer and passionate open source contributor with around 15 years of experience in Information Technology. His current focus is on web applications, and he frequently works with AngularJS. He also founded the Hamburg AngularJS Meetup. You can find his open source projects on https://github.com/matthiasn and his blog at http://matthiasnehlsen.com. You can also follow him on Twitter at @matthiasnehlsen.

    Browse publications by this author

Latest Reviews

(3 reviews total)
Good
Excellent
Excellent