The Anatomy of an AngularJS App: Using Modules to Define AngularJS Components

The angular.module method returns a Module object that provides access to the most important features that AngularJS provides via the properties and methods I have described in Table 9-3. As I explained at the start of this chapter, the features that the Module object provides access to are the features that I spend a lot of this book describing. I provide a brief demonstration and explanation of the most important features in the section and include references to the chapters in which you can find more details.

The methods defined by the Module object fall into three broad categories: those that define components for an AngularJS application, those that make it easier to create those building blocks, and those that help manage the AngularJS life cycle. I’ll start by introducing the building blocks and then talk about the other features that are available.

1. Defining Controllers

Controllers are one of the big building blocks of an AngularJS application, and they act as a conduit between the model and the views. Most AngularJS projects will have multiple controllers, each of which delivers the data and logic required for one aspect of the application. I describe controllers in depth in Chapter 13.

Controllers are defined using the Module.controller method, which takes two arguments: the name of the controller and a factory function, which is used to set up the controller and get it ready for use (see the “Factory and Worker Functions” sidebar later in the chapter for more details). Listing 9-4 shows the statements from the example.html file that create the controller.

Listing 9-4. Creating a Controller in the example.html File

myApp.controller(“dayCtrl”, function ($scope) {

// controller statements will go here

});

The convention for controller names is to use the suffix Ctrl. The statement in the listing creates a new controller called dayCtrl. The function passed to the Module.controller method is used to declare the controller’s dependencies, which are the AngularJS components that the controller requires. AngularJS provides some built-in services and features that are specified using argument names that start with the $ symbol. In the listing you can see that I have specified the $scope, which asks AngularJS to provide the scope for the controller. To declare a dependency on $scope, I just have to use the name as an argument to the factory function, like this:

myApp.controller(“dayCtrl”, function ($scope) {

This is an example of dependency injection (DI), where AngularJS inspects the arguments that are specified for a function and locates the components they correspond to; see the “Understanding Dependency Injection” sidebar for details. The function I passed to the controller method has an argument called x, and AngularJS will automatically pass in the scope object when the function is called. I explain how services work in Chapter 18 and show you how scopes work in Chapter 13.

1.1. Applying Controllers to Views

Defining controllers is only part of the process—they must also be applied to HTML elements so that AngularJS knows which part of an HTML document forms the view for a given controller. This is done through the ng-controller attribute, and Listing 9-5 shows the HTML elements from the example.html file that apply the dayCtrl controller to the HTML document.

Listing 9-5. Defining Views in the example.html File

<body>

<div class=”panel” ng-controller=”dayCtrl”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div>

<h4>Today is {{day || “(unknown)”}}</h4>

</div>

</body>

The view in this example is the div element and its contents—in other words, the element to which the ng-controller attribute has been applied and the elements it contains.

The $scope component that I specified as an argument when I created the controller is used to provide the view with data, and only the data configured via $scope can be used in expressions and data bindings. At the moment, when you navigate to the example.html file with the browser, the data binding generates the string (unknown) because I have used the || operator to coalesce null values, like this:

<h4>Today is {{day || “(unknown)”}}</h4>

A nice feature of AngularJS data bindings is that you can use them to evaluate JavaScript expressions. This binding will display the value of the day property provided by the $scope component unless it is null, in which case (unknown) will be displayed instead. To provide a value for the day property, I must assign it to the $scope in the controller setup function, as shown in Listing 9-6.

Listing 9-6. Defining a Model Data Value in the example.html File

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Nonday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[new Date().getDay()];

});

</script>

I create a new Date, call the getDay method to get the numeric day of the week, and look up the day name from an array of string values. As soon as I make this addition to the script element, the value I have specified is available to the view and is used in the HTML output, as shown in Figure 9-2.

1.2. Creating Multiple Views

Each controller can support multiple views, which allows the same data to be presented in different ways or for closely related data to be created and managed efficiently. In Listing 9-7, you can see how I have added a data property to $scope and created a second view that takes advantage of it.

Listing 9-7. Adding a Second View to the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>AngularJS Demo</title>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”angular.js”></script>

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Monday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[new Date().getDay()];

$scope.tomorrow = dayNames[(new Date().getDay() + 1) %

});

</script>

</head>

<body>

<div class=”panel”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div>

<h4 ng-controller=”dayCtrl”>Today is {{day || “(unknown)”}}</h4>

<h4 ng-controller=”dayCtrl”>Tomorrow is {{tomorrow || “(unknown)”}}</h4>

</div>

</body>

</html>

I have moved the ng-controller attribute so that I can create two simple views side-by-side in the HTML document; you can see the effect in Figure 9-3.

I could have achieved the same result within a single view, of course, but I want to demonstrate different ways in which controllers and views can be used.

1.3. Creating Multiple Controllers

All but the simplest applications will contain multiple controllers, each of which will be responsible for a different aspect of the application functionality. Listing 9-8 shows how I have added a second controller to the example.html file.

Listing 9-8. Adding a Second Controller to the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>AngularJS Demo</title>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”angular.js”></script>

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Monday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[new Date().getDay()];

});

myApp.controller(“tomorrowCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Nonday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[(new Date().getDay() + 1) % l];

})>

</script>

</head>

<body>

<div class=”panel”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div>

<h4 ng-controller=”dayCtrl”>Today is {{day || “(unknown)”}}</h4>

<h4 ng-controller=”tomorrowCtrl”>Tomorrow is {{day || “(unknown)”}}</h4>

</div>

</body>

</html>

I have added a controller called tomorrowCtrl that works out tomorrow’s name. I have also edited the HTML markup so that each controller has its own view. The result of these changes is the same as shown in Figure 9-3—only the way the content is generated differs.

Tip Notice how I am able to use the day property in both views without the values interfering with each other. Each controller has its own part of the overall application scope, and the day property of the dayCtrl controller is isolated from the one defined by the tomorrowCtri controller. I describe scopes in Chapter 13.

You would not create two controllers and two views for such a simple app in the real world, but I want to demonstrate the different features of modules, and this is a good foundation for doing so.

2. Defining Directives

Directives are the most powerful AngularJS feature because they extend and enhance HTML to create rich web applications. There are lots of features to like in AngularJS, but directives are the most enjoyable and flexible to create. I describe the built-in directives that come with AngularJS in Chapters 10-12, but you can also create your own custom directives when the built-in ones don’t meet your needs. I explain this process in detail in Chapters 15-17, but the short version is that custom directives are created via the Module.directive method. You can see a simple example of a custom directive in Listing 9-9.

Listing 9-9. Creating a Custom Directive in the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>AngularJS Demo</title>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”angular.js”></script>

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Monday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[new Date().getDay()];

});

myApp.controller(“tomorrowCtrl”, function ($scope) {

var dayNames = [“Sunday”, “Monday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

$scope.day = dayNames[(new Date().getDay() + 1) % 7];

});

myApp.directive(“highlight”, function () {

return function (scope, element, attrs) {

if (scope.day == attrs[“highlight”]) {

element.css(“color”, “red”);

}

}

});

</script>

</head>

<body>

<div class=”panel”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div>

<h4 ng-controller=”dayCtrl” highlight=”Nonday”>

Today is {{day || “(unknown)”}}

</h4>

<h4 ng-controller=”tomorrowCtrl”>Tomorrow is {{day || “(unknown)”}}</h4>

</div>

</body>

</html>

There are different ways to create a custom directive, and the listing shows one of the simplest. I have called the Module.directive method, providing the name of the directive I want to create and a factory function that creates the directive.

2.1. Applying Directives to HTML Elements

The factory function in this example is responsible for creating a directive, which is a worker function that AngularJS calls when it encounters the directive in the HTML. To understand how a custom directive works, it helps to start with the way it is applied to an HTML element, as follows:

<h4 ng-controller=”dayCtrl” highlight=”Monday“>

My custom directive is called highlight, and it is applied as an attribute (although there are other options—such as custom HTML elements—as I describe in Chapter 16). I have set the value of the highlight attribute to be Monday. The purpose of my custom directive is to highlight the contents of the element that it is applied to if the day model property corresponds to the attribute value.

The factory function I passed to the directive method is called when AngularJS encounters the highlight attribute in the HTML. The directive function that the factory function creates is invoked by AngularJS and is passed three arguments: the scope for the view, the element to which the directive has been applied, and the attributes of that element.

Tip Notice that the argument for the directive function is scope and not $scope. I explain why there is no $ sign and what the difference is in Chapter 15.

The scope argument lets me inspect the data that is available in the view; in this case, it allows me to get the value of the day property. The attrs argument provides me with a complete set of the attributes that have been applied to the element, including the attribute that applies the directive: I use this to get the value of the highlight attribute.

If the value of the highlight attribute and the day value from the scope match, then I use the element argument to configure the HTML content.

The element argument is a jqLite object, which is the cut-down version of jQuery that is included with AngularJS. The method I used in this example—css—sets the value of a CSS property. By setting the color property, I change the color of the text for the element. I explain the complete set of jqLite methods in Chapter 15. You can see the effect of the directive in Figure 9-4 (although you will have to change the value of the highlight attribute if you are not running the example on a Monday).

3. Defining Filters

Filters are used in views to format the data displayed to the user. Once defined, filters can be used throughout a module, which means you can use them to ensure consistency in data presentation across multiple controllers and views. In Listing 9-10, you can see how I have updated the example.html file to include a filter, and in Chapter 14,

I explain the different ways that filters can be used, including using the built-in filters that come with AngularJS.

Listing 9-10. Adding a Filter to the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>AngularJS Demo</title>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”angular.js”></script>

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope) {

$scope.day = new Date().getDay();

});

myApp.controller(“tomorrowCtrl”, function ($scope) {

$scope.day = new Date().getDay() + 1;

});

myApp.directive(“highlight”, function () {

return function (scope, element, attrs) {

if (scope.day == attrs[“highlight”]) {

element.css(“color”, “red”);

}

}

});

myApp.filter(“dayName”, function () {

var dayNames = [“Sunday”, “Nonday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

return function (input) {

return angular.isNumber(input) ? dayNames[input] : input;

};

});

</script>

</head>

<body>

<div class=”panel”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div> <h4 ng-controller=”dayCtrl” highlight=”Monday”>

Today is {{day || “(unknown)” | dayName}}

</h4>

<h4 ng-controller=”tomorrowCtrl”>

Tomorrow is {{day || “(unknown)” \ dayName}}

</h4>

</div>

</body>

</html>

The filter method is used to define a filter, and the arguments are the name of the new filter and a factory function that will create the filter when invoked. Filters are themselves functions, which receive a data value and format it so it can be displayed.

My filter is called dayName, and I have used it to consolidate the code that transforms the numeric day of the week that I get from the Date objects into a name. My factory function defines the array of weekday names and returns a function that uses that array to transform numeric values:

return function (input) {

return angular.isNumber(input) ? dayNames[input] : input;

};

I use the angular.isNumber method that I described in Chapter 5 to check that I am dealing with a numeric value and return the day name if I am. (To keep the example simple, I am not checking for values that are out of bounds.)

3.1. Applying Filters

Filters are applied in template expressions contained in views. The data binding or expression is followed by a bar (the | character) and then the name of the filter, as follows:

<h4 ng-controller=”dayCtrl” highlight=”Monday”>

Today is {{day || “(unknown)” | dayName}}

</h4>

Filters are applied after JavaScript expressions are evaluated, which allows me to use the || operator to check for null values and then the | operator to apply the filter. The result of this is that the value of the day property will be passed to the filter function if it is not null, and if it is, then (unknown) will be passed instead, which is why I used the isNumber method.

3.2. Fixing the Directive

If you have sharp eyes, you may have noticed when I added the filter, I managed to break the directive I created earlier. This is because my controllers now add a numeric representation of the current day to their scopes, rather than the formatted name. My directive checks for the value Monday, but it will only ever find 1, 2, and so on, and so will never highlight the date.

AngularJS development is full of little challenges like this because you will often refactor your code to move functionality from one component to another, just as I removed the name formatting from the controllers to a filter. There are several ways to solve this problem—including updating the directive to use numeric values as well—but the solution I want to demonstrate is a little more complex. In Listing 9-11, you can see the modifications I made to the definition of the directive.

Listing 9-11. Changing the Directive in the example.html File

myApp.directive(“highlight”, function ($filter) {

var dayFilter = $filter(“dayName”);

return function (scope, element, attrs) {

if (dayFilter(scope.day) == attrs[“highlight”]) {

element.css(“color”, “red”);

}

}

});

What I want to demonstrate with this example is that the building blocks that you create in an AngularJS application are not just limited to use on HTML elements; you can also use them in your JavaScript code.

In this case, I have added a $filter argument to my directive factory function, which tells AngularJS that I want to receive the filter service object when my function is called. The $filter service gives me access to all of the filters that have been defined, including my custom addition from the previous example. I obtain my filter by name, like this:

var dayFilter = $filter(“dayName”);

I receive the filter function that my factory creates, and I can then call that function to transform my numeric value to a name:

if (dayFilter(scope.day) == attrs[“highlight”]) {

With this change, my directive works again. There are two important points to note in this example. The first is that refactoring code is a natural part of the AngularJS development process, and the second is that AngularJS makes refactoring easier by providing both declarative (via HTML) and imperative (via JavaScript) access to the building blocks you create.

4. Defining Services

Services are singleton objects that provide any functionality that you want to use throughout an application. There are some useful built-in services that come with AngularJS for common tasks such as making HTTP requests. Some key AngularJS are delivered as services, including the $scope and $filter objects that I used in the earlier example. Since this is AngularJS, you can create your own services, a process that I demonstrate briefly here and describe in depth in Chapter 18.

Tip Singleton means that only one instance of the object will be created by AngularJS and shared between the parts of the application that need the service.

Three of the methods defined by the Module object are used to create services in different ways: service, factory, and provider. All three are closely related, and I’ll explain the differences between them in Chapter 18.

For this chapter, I am going to use the service method to create a basic service to consolidate some of the logic in my example, as shown in Listing 9-12.

Listing 9-12. Creating a Simple Service in the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>AngularJS Demo</title>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”angular.js”></script>

<script>

var myApp = angular.module(“exampleApp”, []);

myApp.controller(“dayCtrl”, function ($scope, days) {

$scope.day = days.today;

});

myApp.controller(“tomorrowCtrl”, function ($scope, days) {

$scope.day = days.tomorrow;

});

myApp.directive(“highlight”, function ($filter) {

var dayFilter = $filter(“dayName”);

return function (scope, element, attrs) {

if (dayFilter(scope.day) == attrs[“highlight”]) {

element.css(“color”, “red”);

}

}

});

myApp.filter(“dayName”, function () {

var dayNames = [“Sunday”, “Monday”, “Tuesday”, “Wednesday”,

“Thursday”, “Friday”, “Saturday”];

return function (input) {

return angular.isNumber(input) ? dayNames[input] : input;

};

});

myApp.service(“days”, function () {

this.today = new Date().getDay();

this.tomorrow = this.today + 1;

});

</script>

</head>

<body>

<div class=”panel”>

<div class=”page-header”>

<h3>AngularJS App</h3>

</div>

<h4 ng-controller=”dayCtrl” highlight=”Monday”>

Today is {{day || “(unknown)” | dayName}}

</h4>

<h4 ng-controller=”tomorrowCtrl”>

Tomorrow is {{day || “(unknown)” | dayName}}

</h4>

</div>

</body>

</html>

The service method takes two arguments: the name of the service and a factory function that is called to create the service object. When AngularJS calls the factory function, it assigns a new object that is accessible via the this keyword, and I use this object to define today and tomorrow properties. This is a simple service, but it means I can access the today and tomorrow values via my service anywhere in my AngularJS code—something that helps simplify the development process when creating more complex applications.

Tip Notice that I am able to use the server from within the controllers, even though I call the service method after I call the controller method. You can create your component in any order, and AngularJS will ensure that everything is set up correctly before it starts calling factory functions and performing dependency injection. See the “Working with the AngularJS Life Cycle” section later in this chapter for more details.

I access my service by declaring a dependency for my days service, like this:

myApp.controller(“tomorrowCtrl”, function ($scope, days) {

AngularJS uses dependency injection to locate the days service and pass it as an argument to the factory function, which means I can then get the value of the today and tomorrow properties and use the $scope service to pass them to the view:

myApp.controller(“tomorrowCtrl”, function ($scope, days) {

$scope.day = days.tomorrow;

});

I show you the other ways of creating services—including how to use the service method to take advantage JavaScript prototypes—in Chapter 18.

4.1. Defining Values

The Module.value method lets you create services that return fixed values and objects. This may seem like an odd thing to do, but it means you can use dependency injection for any value or object, not just the ones you create using module methods like service and filter. It makes for a more consistent development experience, simplifies unit testing, and allows you to use some advanced features, like decoration, which I describe in Chapter 24. In Listing 9-13, you can see how I have modified the example.html file to use a value.

Listing 9-13. Defining a Value in the example.html File

<script>

var myApp = angular.module(“exampleApp”, []);

// …statements omitted for brevity…

var now = new Date(); myApp.value(“nowValue”, now);

myApp.service(“days”, function (nowValue) {

this.today = nowValue.getDay();

this.tomorrow = this.today + 1;

});

</script>

In this listing I have defined a variable called now. I have assigned a new Date to the variable and then called the Module.value method to create the value service, which I called nowValue. I then declared a dependency on the nowValue service when I created my days service.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *