© Elad Elrom 2016

Elad Elrom, Pro MEAN Stack Development, 10.1007/978-1-4842-2044-3_5

5. AngularJS

Elad Elrom

(1)New York, USA

AngularJS is the “A” in the MEAN stack. AngularJS enables developers to create front-end rich client side applications . The pieces are loosely coupled and structured in a modular fashion, resulting in less code to write, added flexibility, easier-to-read code, and quicker development time.

At the end of the day, AngularJS simply allows the developer to put together a toolset for building a framework, which will fit your exact application’s needs. Additionally, AngularJS is well structured and built to be fully accessible, in accordance with Accessible Rich Internet Applications (ARIA ) so your app or site can be built correctly for people with disabilities. AngularJS also gets along very well with other JavaScript libraries; its features can be easily modified or replaced to fit your exact needs.

We will be covering AngularJS throughout this chapter, diving deep into one of its most popular skeleton projects, called “Angular Seed .” We will be looking at each piece individually, which will allow us to gain a full understanding of the AngularJS framework. Much of the work we have done in past chapters will come in handy here, since we won’t need to do much installation and we can hit the ground running right away.

Note

The word “Angular” means having multiple angles or measured by an angle. Visit https://angularjs.org/ to learn more about AngularJS.

An AngularJS best practice is using a Model View Controller (MVC) style architecture , and in fact, AngularJS supports coding with seperation of concerns. Splitting code into different piles is practiced in most programming languages, and applies to AngularJS as well.

Note

AngularJS MVC includes the model, which is the application’s data; the view, which is the HTML and directives (more on these later in this chapter), and the controller, which is the glue holding the model and the view together. The controller takes the data, applies business logic, and sends the results to the view.

The current version of AngularJS is version 1, AKA AngularJS1 , however in 2015 a developer preview of AngularJS2 was released, and it’s in Release Candidate 5 (RC5) status at the time of this writing. There are differences between the two versions, and it’s a good idea to get familiar with AngularJS version 2 now to be able to migrate your code if you ever need to.

AngularJS2’s goal is for users to be able to use the same code to develop across all platforms, which aligns very well with this book and will make it easier for us to upgrade to Angular 2.

Angular Seed Project

You can create your project from scratch, download the necessary libraries, test, build scripts, and create your own folder structure, or you can download a skeleton project to quickly bootstrap your project. You can use a pre-set dev environment so that you can begin developing quickly and efficiently, following AngularJS’s best practices.

There are pros and cons of using a boilerplate skeleton code . You can decide on your own if you want to use this skeleton for future projects, but it is a good way to start your first AngularJS app and learn the ropes.

A good example of a project boilerplate skeleton project to use is “angular-seed”; visit the page on GitHub to learn more about the project: https://github.com/angular/angular-seed .

“Angular-seed” includes the AngularJS framework as well as development and testing tools to help you start coding immediately.

WebStorm already includes a copy of angular-seed, so you can get up and running quickly. Open WebStorm and close any open projects, then wait for the welcome screen to come up.

On the welcome screen, select “Create New Project” ➤ “AngularJS” from the left menu—see Figure 5-1.

A416092_1_En_5_Fig1_HTML.jpg
Figure 5-1. Create New Project: Choose Project Directory window

Under “New Project” choose the location and name of the app as “angular-seed.” Click “Create” and the new project opens up—see Figure 5-2.

A416092_1_En_5_Fig2_HTML.jpg
Figure 5-2. “angular-seed” project in WebStorm

Now that we have the project downloaded, we can fetch all of the libraries we will need. “Angular-seed” is set with a preconfigured npm command to automatically run and install all of these libraries. In the command line, simply type the following in the location of the project:

$ sudo npm install
$ sudo bower install --allow-root

That’s it! These commands install all the libraries we need from npm and Bower. Now we have all we need to start our project. To run the project, all we have to do is run this command:

$ sudo npm start

Navigate to the following URL, or use the “open” command in the second command line to see the app. See Figure 5-1.

$ open http://localhost:8000

Now, the project is ready (see Figure 5-3). If you want to shut down the server, click ctrl+c.

A416092_1_En_5_Fig3_HTML.jpg
Figure 5-3. angular-seed My AngularJS app showing in a Google Chrome browser

Now that we have the project set up in WebStorm, we can run the project commands from the WebStorm Terminal window. Type “npm start” instead of using the Mac Terminal application—see the WebStorm Terminal window in Figure 5-2.

Let’s now dive into the files and libraries that the Angular-seed project includes.

Bower Component

The first folder under the “app” root folder is the “bower_components” folder, located here: “angular-seed/app/bower_components.” When we run the command “npm install”, it’s actually running the “Bower install” command for us.

Open “angular-seed/bower.json” and you will see what has been installed for us in the Bower component folder :

  "dependencies": {
    "angular": "∼1.5.0",
    "angular-route": "∼1.5.0",
    "angular-loader": "∼1.5.0",
    "angular-mocks": "∼1.5.0",
    "html5-boilerplate": "^5.3.0"
  }

The “bower_components” folder includes the following libraries, and if you open the “README.md” file in each library, you can retrieve all of the information regarding that library. As you may recall, we have already configured WebStorm with a Markdown plugin, so when you open the “README.md” file , it should open in its pre-styled form.

  • Angular: This is an AngularJS framework code base.

  • Angular-loader: This is the AngularJS loader library. It will allow your AngularJS scripts to be loaded in any order you want them. To implement this, place it on top of your index file (so that it's executed first) and proceed to load your files in any order you want. There are other libraries that can be used in order to achieve the same goal, such as “RequireJS” or “angular-async-loader,” or you can use more modern libraries such as “Browserify” or “webpack,” but feel free to search through these libraries online and explore.

  • Angular-mocks: The library is built to replace functionality for testing purposes. An example is the AngularJS $http service internal API, which is mocked by AngularJS's built-in $httpBackend in ngMocks. Why would you need this? You are able to create a mocked service and use the results in tests. If the backend is tested properly and the interfaces are defined clearly, there will be a clean separation of concerns and the testing strategy will be efficient.

  • Angular-route: The $route library is used for deep-linking URLs to controllers and views. $route API listens for $location.url() change event and then tries to map a path to route definition; more about that later in this chapter.

  • html5-boilerplate: HTML5 boilerplate code is aimed at enabling you to build faster and more enterprise-grade web apps or sites. You can check the project and see what it contains here: https://html5boilerplate.com/ . The HTML5 boilerplate includes analytics, icons, Normalize.css, jQuery, and Modernizr configured Apache setting codes to increase performance.

App Layout File

The “index.html” file is the app entry point layout file (the main HTML template file of the app). It holds the Bower components, styles, routes, controllers, directives, filters, and components. See Listing 5-1.

Listing 5-1. App layout file index.html
  <div ng-view></div>

  <div>Angular seed app: v<span app-version></span></div>

  <!-- In production use:
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/x.x.x/angular.min.js"></script>
  -->
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="app.js"></script>
  <script src="view1/view1.js"></script>
  <script src="view2/view2.js"></script>
  <script src="components/version/version.js"></script>
  <script src="components/version/version-directive.js"></script>
  <script src="components/version/interpolate-filter.js"></script>

When using the AngularJS script tag, AngularJS will be integrated and initialized automatically.

Notice that the scripts are set at the bottom of the page, which improves the app load time. If the scripts had been at the top, HTML loading would have been blocked by the loading of the JS scripts.

It is recommended that you place the “ng-app” at the root of the app, usually on the “html” tag. This will enable AngularJS to bootstrap your app automatically. Also, it is a good idea to add the “ng-strict-di” directive on the same element as “ng-app” to the HTML tag to ensure the app is annotated correctly. It is recommended that you enable “ng-strict-di” in all of your scripts as soon as possible in order to prevent the app from breaking later on when the annotation is added.

Note

To ensure that your app will be able to be minified once ready for deployment, a good practice is using “strict-di.” The app must fail to run when invoking functions that don’t use explicit function annotation. This means that the methods used must be declared. Using the “ng-strict-di” will ensure the app is confirming with dependency injection guidelines and will fail to run if not.

You can achieve this by using:

<html ng-app="myApp" ng-strict-di>

Add the following to each HTML tag:

<!DOCTYPE html>
<!--[if lt IE 7]>      <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8 lt-ie7" ng-strict-di> <![endif]-->
<!--[if IE 7]>         <html lang="en" ng-app="myApp" class="no-js lt-ie9 lt-ie8" ng-strict-di> <![endif]-->
<!--[if IE 8]>         <html lang="en" ng-app="myApp" class="no-js lt-ie9" ng-strict-di> <![endif]-->
<!--[if gt IE 8]><!--> <html lang="en" ng-app="myApp" class="no-js" ng-strict-di> <!--<![endif]-->

The other entry layout file, “index-async.html,” holds the same content as “index.html,” but it loads the JS files asynchronously instead of synchronously.

These two index files are interchangeable, and either one can be used to build your app. “index-async.html” loads scripts asynchronously, which usually gives you a faster bootstrap time, while “index.html” loads scripts synchronously, which is often a bit slower, but its code is easier to understand for someone new to the AngularJS framework (Listing 5-2).

Listing 5-2. App layout file index-async.html
<script>
    // load all of the dependencies asynchronously.
    $script([
      'bower_components/angular/angular.js',
      'bower_components/angular-route/angular-route.js',
      'app.js',
      'view1/view1.js',
      'view2/view2.js',
      'components/version/version.js',
      'components/version/version-directive.js',
      'components/version/interpolate-filter.js'
    ], function() {
      // when all is done, execute bootstrap angular application
      angular.bootstrap(document, ['myApp']);
    });
  </script>
  <title>My AngularJS App</title>
  <link rel="stylesheet" href="app.css">
</head>
<body ng-cloak>
  <ul class="menu">
    <li><a href="#/view1">view1</a></li>
    <li><a href="#/view2">view2</a></li>
  </ul>


  <div ng-view></div>

  <div>Angular seed app: v<span app-version></span></div>

</body>
</html>

AngularJS offers a bootstrap guide, which allows you to learn more about the initialization process—specifically, how you can manually initialize an AngularJS project when necessary:

https://docs.angularjs.org/guide/bootstrap

In order for “index-async.html” to work, you will need to inject a piece of AngularJS JavaScript code into the HTML page. Luckily for Angular-seek, the project has a predefined script to help you do that. In the command line, just type the following code:

$ npm run update-index-async

The npm script copies the contents of the “angular-loader.js” file into the “index-async.html” HTML page. Note that you will need to run this script every time you update a version of AngularJS.

There are times that you will need to have even more control over the AngularJS framework and you may want to manually initialize AngularJS. Here is a simple example of manually initializing AngularJS (see Listing 5-3).

Listing 5-3. Manually initialize AngularJS
<!DOCTYPE html>
<html>
<body>
<script src="bower_components/angular/angular.js"></script>


  <script>
    angular.module('myApp', [])
      .controller('MyController', ['$scope', function ($scope) {
        // TODO
      }]);


    angular.element(document).ready(function() {
      angular.bootstrap(document, ['myApp']);
    });
  </script>
</body>
</html>

Notice that, only once the AngularJS element received the “ready” event will be dispatch result in bootstrap of AngularJS.

Partial Views

Creating partial views allows you to split the view into separate files. Think of each piece as a stand-alone reusable UI module. “Angular-seed” promotes this type of architecture and comes with two partial templates :

  1. angular-seed/app/view1/view1.html

  2. angular-seed/app/view2/view2.html

“view1.html” holds a paragraph tag with a copy—see below:

<p>This is the partial for view 1.</p>

“view2.html” holds a paragraph tag and a binding tag along with the version of the app—see below:

<p>This is the partial for view 2.</p>
<p>
  Showing of 'interpolate' filter: {{ 'Current version is v%VERSION%.' | interpolate }}
</p>

“Angular-seed” gives us a taste of the data binding’s feature available in AngularJS. The code implements two-way data binding and connects the “view” to your “model” seamlessly using a reflection, meaning that once you change the JavaScript object (model), the HTML code (view) will be updated automatically. The result? View is updated without the need for your own DOM manipulation or event handling .

Styles

“app.css” is the default stylesheet for the app. It can be used as the starting point for you to build upon. See the CSS code created for us in Listing 5-4.

Listing 5-4. app.css stylesheet
.menu {
  list-style: none;
  border-bottom: 0.1em solid black;
  margin-bottom: 2em;
  padding: 0 0 0.5em;
}


.menu:before {
  content: "[";
}


.menu:after {
  content: "]";
}


.menu > li {
  display: inline;
}


.menu > li:before {
  content: "|";
  padding-right: 0.3em;
}


.menu > li:nth-child(1):before {
  content: "";
  padding: 0;
}

Controllers

The concept of controller is not new; it’s the “C” in the “MVC” (“Model-View-Controller”) pattern and acts as the glue between the view (HTML) and model (JavaScript object). In AngularJS, the controller is defined by a JavaScript constructor function that used to augment what AngularJS calls a $scope .

Note

In AngularJS, the $scope is the owner or context of the application’s variables and functions. Once the $scope is created, you can then create objects in the controller’s $scope, such as variables and functions. The “ng-model” directives can then use the variables and functions in that context.

Behind the scenes, when a controller is attached to the DOM via the “ng-controller” directive, the AngularJS instantiates the controller's constructor function you created, all of which is done automatically for you.

When you create a new controller, a new child $scope is created and can be used as an injectable parameter to the controller's constructor function. See code below as example:

.controller('View1Ctrl', ['$scope', function($scope) {
  // Implement
}]);

According to AngularJS’s guides, controllers should be used to:

  1. Set up the initial state of $scope object.

  2. Add behavior to the $scope object.

Common implementation mistakes regarding AngularJS Controllers include:

  • Formatting input: Do not use controllers to format <input> tags. This can be done inside AngularJS form controls.

  • Filtering output: Do not filter output or results inside of controllers—use filters (more about filters later in this chapter).

  • Sharing state: Do not share code or state across multiple controllers. You can use services instead (more about service in future chapters).

  • Manipulating the DOM: Controllers should contain business logic only; it’s the “C” in the MVC pattern. Putting view logic into controllers can break the AngularJS MVC framework and reduce testability. As we mentioned, use reflection (data binding) or directives to manipulate the DOM.

These are some common mistakes that developers often make in regards to any MVC. They can be seen in all MVC frameworks where MVC is implemented regardless of the language. It’s a good idea to become familiar with MVC patterns— that way, AngularJS will make much more sense.

Take a look at “view1” controller logic. The code is located here: “angular-seed/app/view1/view1.js”. See Listing 5-5.

Listing 5-5. Content of view.js
'use strict';

angular.module('myApp.view1', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/view1', {
    templateUrl: 'view1/view1.html',
    controller: 'View1Ctrl'
  });
}])


.controller('View1Ctrl', [function() {

}]);

Similarly, “view2,” located here “angular-seed/app/view2/view2.js”, holds the controller logic for “view2.” See Listing 5-6.

Listing 5-6. Content of view2.js
'use strict';

angular.module('myApp.view2', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/view2', {
    templateUrl: 'view2/view2.html',
    controller: 'View2Ctrl'
  });
}])


.controller('View2Ctrl', [function() {

}]);

AngularJS sets the controls “View1Controller” and “View2Controller” and initiates them. “Angular-seed” leaves the controllers unimplemented but all ready to be coded.

The $scope becomes available to the “view1” and “view2” templates when the controllers are registered. The following example implements “View1Controller,” which attaches the greeting property “Hello!” to the $scope.

Change “view1.js” from the following:

.controller('View1Ctrl', [function() {
}]);

To the implemented code:

.controller('View1Ctrl', ['$scope', function($scope) {
  $scope.greeting = 'Hello!';
}]);

Now that we have added a variable, we can use the “greeting” property in “view1.html,” using binding, to change “view1.html” from:

<p>This is the partial for view 1.</p>

To the following code:

<div ng-controller="View1Ctrl">
    <p>This is the partial for view 1.</p>
    {{ greeting }}
</div>

Run the app by typing in the Bash command line the following commands, in two separate Bash terminal windows:

$ npm start
$ open http://localhost:8000/app/index.html

As mentioned before, it’s good practice to open two tabs for the Terminal in WebStorm so that you can type commands without leaving your IDE. The browser opens up to the following URL:

http://localhost:8000/app/index.html

You can see the binding of the word “Hello!” to the template “view1” (see Figure 5-4).

A416092_1_En_5_Fig4_HTML.jpg
Figure 5-4. angular-seed project implementing controller for “view1”

We can extend the code and create a custom greeting input box with a button. To submit the change of the greeting, paste the following code into “view1.html.” See Listing 5-7.

Listing 5-7. view1.html including custom greeting.
<div ng-controller="View1Ctrl">
    <p>This is the partial for view 1.</p>
    <input data-ng-model="customGreeting">
    <button data-ng-click="changeGreeting(customGreeting)">Change Greeting</button>


    <p>{{ greeting }}</p>
</div>

Notice that the code is using “data-ng-click” and “data-ng-model,” which is the recommended prefix according to the Angulara guide.

Note

“data-ng-model,” “ng:model,” “ng_model,” or even “x-ng-model” are all interchangeable, and the element has matches “ngModel” API for all these examples, the same works for other controller tags. The AngularJS HTML compiler digests all of these the same way, but the AngularJS guide recommends using the data-prefixed “data-ng-model” version. The other forms shown above are accepted for legacy reasons, but AngularJS advises us to avoid using them, so it’s better to get into the habit from day one.

As you can see, we added a “customGreeting” function that is marked as an “ng-model” and an “ng-click” to be mapped to the “changeGreeting” function, allowing us to pass the “customGreeting” variable to the controller.

In “view1.js,” copy the code in Listing 5-8.

Listing 5-8. view1.js controller including custom greeting code
'use strict';

angular.module('myApp.view1', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/view1', {
    templateUrl: 'view1/view1.html',
    controller: 'View1Ctrl'
  });
}])


.controller('View1Ctrl', ['$scope', function($scope) {
  $scope.greeting = 'Hello!';
  $scope.customGreeting = "Hello Angular";
  $scope.changeGreeting = function(output) {
  $scope.greeting = output;
};
}]);

Notice that we added two objects—a “customGreeting” variable and a “changeGreeting” function—to be used to update the greeting variable.

Refresh the page and then you can insert a custom greeting. Once you click “Change Greeting,” the text changes accordingly (Figure 5-5).

A416092_1_En_5_Fig5_HTML.jpg
Figure 5-5. angular-seed project implementing controller component for view1

AngularJS Directives

Directives are considered by many to be the most complex concept in AngularJS, but understanding and being able to write your own directives very rewarding. You will have the ability to create your own custom HTML tags with just a few lines of code.

At a high level, directives are markers on a DOM element . These markers can point to any DOM component, from an attribute to an element name, or even a comment or CSS class. These markers then tell the AngularJS’s HTML compiler to attach a specified behavior or to transform the entire DOM element and its children based on a specific logic.

Angular comes with a set of many of these directives already built-in. In fact, we have already used them—in “view1.html” and “view2.html” we used “ngModel” and “ngClick” directives.

Note

AngularJS directives are usually spelled with lowercase letters and camel case—for example,“ngModel” and “ngClick.” You should use the same convention when writing your own custom directives.

See the code below:

<input data-ng-model="customGreeting">

In these directives, AngularJS extends HTML by providing custom directives that add functionality to the markup and allow us to create powerful, dynamic templates. Adding a reflection via data binding has given us a whole new feature.

In addition to the built-in directives, AngularJS allows us to develop our own, creating reusable components to fill our needs. Abstracting away the DOM manipulation logic creates a great separation of high-level code from implementations and makes JavaScript and HTML more object-oriented languages .

“Angular-seed” aids us in creating directives—in fact, it comes with a directive called “version.” The “version” directive includes testing and implementation to get you started, which is tremendously helpful; it comes set with the folder structure and an example you can easily follow.

Let’s examine the “version” directive:

The “version” directive is placed in a folder called “components,” where you are encouraged to place all directives moving forward. Behind the scenes, just like controllers, directives are registered in what is called “module.directive.”

Look at “version.js,” located here: “app/components/version/version.js.” It holds the version module declaration and basic "version" value service. See below:

'use strict';

angular.module('myApp.version', [
  'myApp.version.interpolate-filter',
  'myApp.version.version-directive'
])


.value('version', '0.1');

In the code, there is a reference to “interpolate-filter,” which replaces the token and will be covered more thoroughly later on in this chapter.

Notice that the “module.directive” API was used to register the directive. The “module.directive” takes the normalized directive name, followed by a factory function, which is the actual directive you are registering.

Note

The AngularJS module is defined as a collection of directives, controllers, services, etc. It’s used to register and retrieve modules. All the modules you will be using must be registered before they will work.

Next, look at the “version-directive.js” file. This is the actual custom directive that returns the current app version. See Listing 5-9.

Listing 5-9. Version-directive code
'use strict';

angular.module('myApp.version.version-directive', [])

.directive('appVersion', ['version', function(version) {
  return function(scope, elm, attrs) {
    elm.text(version);
  };
}]);

As you can see, the directive is defined as “appVersion,” and it is used as a factory function. It returns an object with different options to tell the HTML compiler how the directive should behave when it’s matched.

Note

AngularJS factory, service, and provider are similar—the difference is in what you get. Service just executes code, factory returns an object and provider executes the code and calls the $get method.

In the background, the “appVersion” factory function gets invoked once the compiler matches the directive for the first time. You can and should perform any initialization work necessary. In the background, the function is invoked on AngularJS using $injector.invoke, which makes it injectable, just like the controllers.

Once the directive is set, you can look at the implementation. Take a look inside of “index.html” or “index-async.html”—they will have the following declaration:

<div>Angular seed app: v<span app-version></span></div>

The tag “app-version” will be handled by the HTML compiler ($compile) and will attach a specified behavior to that DOM element. Now we see “Angular seed app: v0.1” in each view. Using directive is powerful, since you now have the ability to create your own custom HTML tags.

Template Expanding Directive

Let’s create our own custom directives for AngularJS to use.

Let's say you have a chunk of template that represents some information you want to present internally or to the end user. The template must be repeated many times in the code, and when you change the code in one place, you will want to change it in several others places without copying and pasting or doing a refactoring effort.

This is a good opportunity to utilize a directive type called “Template,” expanding to simplify your code. Let's create a directive that simply replaces the contents of the HTML code with a static template.

First, create a new directive. We will call our first directive <first>—the name doesn’t need to describe the directive functionality since it’s just for practice. The folder structure can follow the <version> directive that came out of the box with Angular-seek.

Add the code from Listing 5-10 into the following structure: app/components/first/first-directive.js

Listing 5-10. First directive returns a template
'use strict';

angular.module('myApp.first.first-directive', [])

    .directive('myFirstDirective', [function() {
        return {
            template: 'Name: {{info.name}} <br /> version: {{info.version}}'
        };
    }]);

As you can see, we are returning a template with the info object that holds the project name and version number. Next, register the directive by pasting Listing 5-11 into app/components/first/first.js:

Listing 5-11.
'use strict';                                                                              

angular.module('myApp.first', [
    'myApp.first.first-directive'
])

Now, we can implement the <first> directive, open app/view2/view2.html and place it in Listing 5-12.

Listing 5-12. View2.html with first dircetive
<p>This is the partial for view 2.</p>
<p>
  Showing of 'interpolate' filter:
  {{ 'Current version is v%VERSION%.' | interpolate }}
</p>
<div my-first-directive></div>
<br />

The div tag with “my-first-directive” will be replaced with the template.

Lastly, don’t forget to add the scripts to the index.html and index-async.html files:

  <script src="components/first/first.js"></script>
  <script src="components/first/first-directive.js"></script>

See the scripts implemented into “index-async.html” in Listing 5-13.

Listing 5-13. index-async.html with first directive scripts
    // load all of the dependencies asynchronously.
    $script([
      'bower_components/angular/angular.js',
      'bower_components/angular-route/angular-route.js',
      'app.js',
      'view1/view1.js',
      'view2/view2.js',
      'components/version/version.js',
      'components/version/version-directive.js',
      'components/version/interpolate-filter.js',
      'components/first/first.js',
      'components/first/first-directive.js'
    ], function() {
      // when all is done, execute bootstrap angular application
      angular.bootstrap(document, ['myApp']);
    });

The best practices in regards to writing directives:

  1. Naming Convention: It is important to avoid a naming convention that may conflict with a potential future standard of HTML or AngularJS. It is recommended that you use your own prefix for directive names. For instance, if you create a <calendar> directive, it may collide with a future releases of HTML—the HTML6 <calendar> tag already exists. Instead, use <myCalendar> or <tripCalendar>. The directive <ngCalendar> may also conflict with a future AngularJS directive.

  2. Coding Convention: It’s recommended that you use a definition object instead of returning a function. As you have seen in <first> directive, we used the “info” object defined in the controller and return a template, not a function.

AngularJS Filters

AngularJS offers filters to help sort data. You can select a subset of items from an array and it will be returned as a new filtered array.

Additionally, AngularJS gives us the ability to create our own custom filters. To create a new filter, we need to first create a filter module and then register the custom filter with the newly created module, just as we’ve done for directives and controllers.

As we’ve seen in the <version> directive that comes with “angular-seed,” it uses a custom filter called “interpolate-filter.js” (Listing 5-14).

Listing 5-14. interpolate-filter.js content
angular-seed/app/components/version/interpolate-filter.js

'use strict';

angular.module('myApp.version.interpolate-filter', [])

.filter('interpolate', ['version', function(version) {
  return function(text) {
    return String(text).replace(/\%VERSION\%/mg, version);
  };
}]);

The filter replaces the value of “version” with a specific version number. As you may recall, “version.js” sets the value of the version:

'use strict';

angular.module('myApp.version', [
  'myApp.version.interpolate-filter',
  'myApp.version.version-directive',
])


.value('version', '0.1');

Components

In AngularJS, a “component” is defined as a special type of directive that uses a simpler configuration. A component works well when you want to create a reusable component based on application structure. This makes it easier to write an app in a way that's similar to Web Components or Angular 2's style of application architecture, so it’s a good idea to start moving in that direction now.

It is important to understand the difference between a directive and a component. Components are triggered by an element, so there’s no need to use components for directives that need to be triggered by attribute or a CSS class. Additionally, we shouldn’t use components when we want to rely on DOM manipulation or are adding event listeners, because the HTML $compile and link functions will not be available when we need advanced directive definition options.

“Angular-seed” is already set with a component folder, as you may recall, and we have the <version> and <first> directives in that directory.

To create and register a component, use the “component” method with the Angular module, just as we’ve done with directives, controllers, and filters.

You can find more information here: https://docs.angularjs.org/guide/component . To learn about components in Angular 2, visit this page:

https://angular.io/docs/ts/latest/tutorial/toh-pt3.html

Testing

In Chapter 3, we configured npm with the “mocha” testing library and were able to run a test simply by typing:

$ npm test

We then terminated the process by hitting ctrl + c—otherwise the test would keep running the process in the background. Mocha tests JavaScripts, but there are times you want to run browser tests.

Karma Testing

“angular-seed” comes with a “Karma” library to run browser tests, and it is already configured, so there’s no need to install anything. To see the code in action, just run “npm test” just as we’ve done with mocha in the Bash command line terminal.

A key feature of Karma is that we can use it to run tests against multiple browsers at the same time. The most common browsers include Chrome, Firefox, Safari, and IE.

When you type “npm test” in the background, it will run the following command: “karma start karma.conf.js”.

The code picks up the testing configurations to be used from the “package.json” script tag:

"scripts": {
  "pretest": "npm install",
  "test": "karma start karma.conf.js",
  "test-single-run": "karma start karma.conf.js  --single-run",
}

To learn more about Karma, check the GitHub open source project located here:

https://github.com/karma-runner/karma

Karma is a highly visible project, and there are plenty of plugins available on npm that extend the functionality of the base code. You can check out the list of plugins here: https://www.npmjs.com/browse/keyword/karma-plugin .

The decision of “angular-seed” to use Karma is not random; the AngularJS team developed Karma, and it is the recommended testing tool for AngularJS projects.

The config file for Karma is called “karma.conf.js.” Luckily, it has already been created for us automatically in the root folder. Open the “karma.conf.js” config file. As you can see, it is set to run the unit tests with Karma (Listing 5-15).

Listing 5-15. karma.conf.js config file
module.exports = function(config){
  config.set({


    basePath : './',

    files : [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/components/**/*.js',
      'app/view*/**/*.js'
    ],


    autoWatch : true,

    frameworks: ['jasmine'],

    browsers : ['Chrome'],

    plugins : [
            'karma-chrome-launcher',
            'karma-firefox-launcher',
            'karma-jasmine',
            'karma-junit-reporter'
            ],


    junitReporter : {
      outputFile: 'test_out/unit.xml',
      suite: 'unit'
    }


  });
};

Notice that “karma.conf.js” is already configured with each library, including AngularJS, Angular-mocks, and the modules:

    files : [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/components/**/*.js',
      'app/view*/**/*.js'
    ],

Next, take a look at the actual test—angular-seed/app/view1/view1_test.js—in Listing 5-16.

Listing 5-16. view1_test.js test file
'use strict';

describe('myApp.view1 module', function() {

  beforeEach(module('myApp.view1'));

  describe('view1 controller', function(){

    it('should ....', inject(function($controller) {
      //spec body
      var view1Ctrl = $controller('View1Ctrl');
      expect(view1Ctrl).toBeDefined();
    }));


  });
});

Notice that the code checks to see that the controller is being defined—the same goes for “view2_test.js” (Listing 5-17).

Listing 5-17. view2_test.js test file
'use strict';

describe('myApp.view2 module', function() {

  beforeEach(module('myApp.view2'));

  describe('view2 controller', function(){

    it('should ....', inject(function($controller) {
      //spec body
      var view2Ctrl = $controller('View2Ctrl');
      expect(view2Ctrl).toBeDefined();
    }));


  });
});

When we run the test using the command line “npm test” in the terminal, the test fails (Figure 5-6).

A416092_1_En_5_Fig6_HTML.jpg
Figure 5-6. Karma tests fails

The tests fails because there is no $scope service, so the $controller provider cannot instantiate the injected $scope argument, so it creates the following error message:

“Karma Unknown provider: $scopeProvider <- $scope”

To fix this, we need to provide the $scope while instantiating a controller using the $controller provider.

Paste the following update in Listing 5-18.

Listing 5-18. view1_test.js file, including creating a new $rootScope instance
'use strict';

describe('myApp.view1 module', function() {

  beforeEach(module('myApp.view1'));

  describe('view1 controller', function(){
          var view1Ctrl, scope;


          beforeEach(inject(function ($controller, $rootScope) {
                  scope = $rootScope.$new();
                  view1Ctrl = $controller("View1Ctrl", {$scope:scope});
          }));


    it('should ....', inject(function() {
      expect(view1Ctrl).toBeDefined();
    }));


  });
});
Note

Notice that we sometimes we use $rootScope and sometimes $scope. The difference between $scope and $rootScope is the context. $rootScope is the parent (“root”) of all the scopes we create in our app, inside all the controllers.

We have then injected the $rootScope into the “setUp" stub of the test and the child $scope is then defined as $rootScope.$new(). Now we are able to inject the $scope as an argument into the $controller constructor as a new child scope:

view1Ctrl = $controller("View1Ctrl", {$scope:scope});

In “view2_test.js”, we will do the same thing. See Listing 5-19.

Listing 5-19. view2_test.js includes creating a new $rootScope instance
'use strict';

describe('myApp.view2 module', function() {

  beforeEach(module('myApp.view2'));

  describe('view2 controller', function(){
           var view2Ctrl, scope;


          beforeEach(inject(function ($controller, $rootScope) {
                  scope = $rootScope.$new();
                  view2Ctrl = $controller("View2Ctrl", {$scope:scope});
           }));


    it('should ....', inject(function() {
      expect(view2Ctrl).toBeDefined();
    }));


  });
});

Press ctrl + c and then run the tests again. Now, the tests will pass (Figure 5-7).

A416092_1_En_5_Fig7_HTML.jpg
Figure 5-7. Karma tests success

To test the <version> directive, take a look at this file: angular-seed/app/version-directive_test.js. See Listing 5-20.

Listing 5-20. directive_test.js test file for version directive
'use strict';

describe('myApp.version module', function() {
  beforeEach(module('myApp.version'));


  describe('app-version directive', function() {
    it('should print current version', function() {
      module(function($provide) {
        $provide.value('version', 'TEST_VER');
      });
      inject(function($compile, $rootScope) {
        var element = $compile('<span app-version></span>')($rootScope);
        expect(element.text()).toEqual('TEST_VER');
      });
    });
  });
});

The test expects that it should print the current version number, and it compares the “TEST_VER” with the element text value to see if they match. We already know that it works, since we have made sure it would pass the tests.

Similarly, there is a code to test the interpolate filter. See the following file: angular-seed/app/interpolate-filter_test.js. Listing 5-21 shows the content of the file:

Listing 5-21. interpolate-filter_test.js test file
'use strict';

describe('myApp.version module', function() {
  beforeEach(module('myApp.version'));


  describe('interpolate filter', function() {
    beforeEach(module(function($provide) {
      $provide.value('version', 'TEST_VER');
    }));


    it('should replace VERSION', inject(function(interpolateFilter) {
      expect(interpolateFilter('before %VERSION% after')).toEqual('before TEST_VER after');
    }));
  });
});

The code expects to interpolate the filter of “%VERSION%” against “TEST_VER.”

Adding New Tests

We have added a new directive <first>, so what we then want to do is to also add a test for our directive template. Add “first-directive_test.js” to the “angular-seed/app/components/first” folder.

This test will ensure that the template has produced the correct text (see Listing 5-22).

Listing 5-22. first-directive_test.js test file
'use strict';

describe('myApp.version module', function() {
  beforeEach(module('myApp.first'));


  describe('app-first directive', function() {
    it('should print the template', function() {
      inject(function($compile, $rootScope) {
        var element = $compile('<div my-first-directive></div>')($rootScope);
        expect(element.text()).toEqual('Name: {{info.name}}  version: {{info.version}}');
      });
    });
  });
});

Once again, run the tests using “npm test” in the shell command line and you should get “Executed 6 of 6 SUCCESS” results.

Proctractor Testing

Protractor is defined as an “end-to-end” testing framework for the AngularJS applications based on Node.js, and as such it is built to make the process of testing easier. Let’s take a look at testing the functionality of an AngularJS app, specifically with Protractor. If you have used Selenium and Selenium WebDriver for creating automated tests, then you will be familiar with how Protractor works. Protractor tests can be run on both regular and headless browsers such as DalekJS or PhantomJS . They are intended to emulate the user’s actions on the application.

Note

End-to-end testing means to test the flow of the application from start to finish. In testing end-to-end, we want to ensure the flow is as expected and that the data is reflected in the view upon change.

This particular testing framework is built with AngularJS apps in mind and can test elements that are specific to the development structure. Protractor testing is also smart enough to automatically wait for a pending task to complete.

Protractor is built on top of WebDriverJS and runs tests against your application in an actual browser, interacting with it as a user would, so if you have ever used behavior-driven development then this should all be familiar to you.

Protractor supports high visibility projects like Jasmine, Mocha, Karma, or Cucumber libraries as well custom frameworks; Jasmine and Mocha are more often used than Cucumber. In fact, Jasmine is set as the default for the Protractor framework.

Take a look at the project in GitHub: https://github.com/angular/protractor

We don’t need to install or configure anything, since everything is already pre-set for us by “angular-seed.”

Open the “package.json” config file; notice that there is a script for “preprotractor” and “protractor.” See below:

"scripts": {
  "preprotractor": "npm run update-webdriver",
  "protractor": "protractor e2e-tests/protractor.conf.js",
}

As you can see “angular-seed” comes with two configuration files , located in the “e2e-tests” folder:

  1. “protractor-conf.js”: Protractor config file

  2. “scenarios.js”: End-to-end scenarios/behavior to be run by Protractor

“Protractor.conf.js” stores each option needed to run such tests, such as timeouts, test directory sources, and suites. See Listing 5-23.

Listing 5-23. protractor.conf.js config file
exports.config = {
  allScriptsTimeout: 11000,


  specs: [
    '*.js'
  ],


  capabilities: {
    'browserName': 'chrome'
  },


  baseUrl: 'http://localhost:8000/app/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  }
};

Scenarios File

The “scenarios.js” file , as, stores the behavior of the app from an end-user standpoint. See Listing 5-24.

Listing 5-24. scenarios.js testing behavior
'use strict';

describe('my app', function() {

  it('should automatically redirect to /view1 when location hash/fragment is empty', function() {
    browser.get('index.html');
    expect(browser.getLocationAbsUrl()).toMatch("/view1");
  });


  describe('view1', function() {

    beforeEach(function() {
      browser.get('index.html#/view1');
    });


    it('should render view1 when user navigates to /view1', function() {
      expect(element.all(by.css('[ng-view] p')).first().getText()).
        toMatch(/partial for view 1/);
    });


  });

  describe('view2', function() {

    beforeEach(function() {
      browser.get('index.html#/view2');
    });


    it('should render view2 when user navigates to /view2', function() {
      expect(element.all(by.css('[ng-view] p')).first().getText()).
        toMatch(/partial for view 2/);
    });


  });
});

As you can see, it checks both view1 and view2 to ensure that the user sees these partials as expected.

To begin the Protractor tests, first we must ensure the project is running in a separate command line shell.

$ sudo npm start

Then, run the following two commands in the command line:

$ sudo npm run update-webdriver
$ sudo npm run protractor

Running these commands may result in an error messages, while running “npm run protractor” on Mac OS X 10.10. However, this can be resolved easily by making a change in “protractor.conf.js.” Simply add the following line:

directConnect: true,

Here is the complete “protractor.conf.js” file content with the change included:

exports.config = {
  allScriptsTimeout: 11000,


  specs: [
    '*.js'
  ],


  capabilities: {
    'browserName': 'chrome'
  },


  directConnect: true,

  baseUrl: 'http://localhost:8000/app/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  }
};

Now, run “npm run protractor” again and the test results should show success, as you can see in Figure 5-8.

A416092_1_En_5_Fig8_HTML.jpg
Figure 5-8. Protractor results

Routes

$route service in AngularJS is used for deep-linking URLs to controllers and HTML partials (view). AngularJS watches $location.url() and then tries to map the path to an existing route definition, which you set.

The $route service is typically used in conjunction with the “ngView” directive and the $routeParams service. The $routeParams service allows you to retrieve the current set of route parameters that were configured.

Open the “app/app.js” main application module file. See Listing 5-25:

Listing 5-25. app.js with first directive
'use strict';

// Declare app level module which depends on views, and components
angular.module('myApp', [
  'ngRoute',
  'myApp.view1',
  'myApp.view2',
  'myApp.version',
  'myApp.first'
]).
config(['$routeProvider', function($routeProvider) {
  $routeProvider.otherwise({redirectTo: '/view1'});
}]);

As you can see from Listing 5-25, the $routeProvider is set to the “otherwise” option, to run “view1” as the default view. If, for example, we want to create a routing of “home,” we can change that easily; all we have to do is set the route to “home.” See Listing 5-26.

Listing 5-26. Adding routeProvider home page
config(['$routeProvider', function($routeProvider) {
  $routeProvider.
  when('/home', {redirectTo: '/view2'}).
  otherwise({redirectTo: '/view1'});
}]);

To test these changes, ensure “npm start” is running, then open the following URL:

localhost:8000/app/index.html#/home

The app will change the URL in the browser automatically to:

http://localhost:8000/app/index.html#/view2

This can come in handy when handling URLs and later on for Search Engine Optimization (SEO) , as you will see in future chapters.

Service

Many apps need content to drive the app. As they say, content is king!

AngularJS XMLHttpRequest (XHR) services create a good separation of MVC and can be considered as an “MVCS” for service, you can create high-level code and low-level implementation. Create a service module, register it just like we’ve done with other modules, then re-use the API calls. This allows you to move your model and business logic out of the front-end code and build back-end agnostic web apps, aligning very well with the MVC pattern. As you’ve seen with data binding, you can then use reflection to display and filter the data in the view.

In Chapter 7, we will be covering building services and we will use these services in an AngularJS app as an example.

Summary

In this chapter, we covered AngularJS. We installed the “angular-seed” project and took a deep dive into fully understanding how it works under the hood. We looked at the Bower components that are installed, Partial Views, CSS styles, controllers, directives, filters, routes, services, and components. We also created our own first directive and implemented it into the view.

We also looked at testing. Angular-seed comes configured with Karma, we looked at the existing tests’ scripts and added new tests, then looked at Protractor testing and executed those tests. Lastly, I also went over best practices, AngularJS 2, and how to prepare and build our app to make it AngularJS2 friendly. Angular guide devoted a section and you can see the difference between AngularJS1 and AngularJS2 here: https://angular.io/docs/ts/latest/cookbook/a1-a2-quick-reference.html

Where to go from here: AngularJS provides a good tutorial, which will walk you through AngularJS and can be found here: https://docs.angularjs.org/tutorial . After being equipped with what you have learned in this chapter, it should now be a breeze for you to review the examples they provide for you and will allow you to expand your knowledge. In the next chapter, we will expand on CSS and responsive design.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.254.44