Working with AngularJS Expressions and Directives

AngularJS provides a set of services that are used to work with AngularJS content and binding expressions. I have described these services in Table 19-8. These services process content into functions that you can then invoke to generate content in your applications, ranging from simple expressions to fragments of HTML that contain bindings and directives.

1. Why and When to Use the Expression and Directive Services

These services can be useful when writing directives because they let you take explicit control of the process used to generate and render content. You won’t need these services in basic directives, but you will find them invaluable when you get into problems that require precise management of templates.

2. Converting Expressions into Functions

The $parse service takes an AngularJS expression and converts it into a function that you can use to evaluate the expression using a scope object. This can be useful in custom directives, allowing the expression to be provided via attributes and evaluated without the directive needing to know the details of the expression. To demonstrate the use of the $parse service, I have added an expressions.html HTML file to the angularjs folder, the contents of which are shown in Listing 19-18.

Listing 19-18. The Contents of the expressions.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Expressions</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.price = “100.23”;

})

.directive(“evalExpression”, function ($parse) {

return function(scope, element, attrs) {

scope.$watch(attrs[“evalExpression”], function (newValue) {

try {

var expressionFn = $parse(scope.expr);

var result = expressionFn(scope);

if (result == undefined) {

result = “No result”;

}

} catch (err) {

result = “Cannot evaluate expression”;

}

element.text(result);

});

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<p><input class=”form-control” ng-model=”expr” /></p>

<div>

Result: <span eval-expression=”expr”></span>

</div>

</div>

</body>

</html>

This example contains a directive called evalExpression that is configured with a scope property that contains an expression that will be evaluated with the $parse service. I have applied the directive to a span element and configured it to use a scope property called expr, which is bound to an input element, allowing an expression to be entered and evaluated dynamically. You can see the effect in Figure 19-8.

So that there is data to work with, I used the controller to add a scope property called price that is set to a numeric value. The figure shows the effect when I enter price | currency in the input element: The price property is processed by the currency filter and the result is displayed as the text content of the span element to which the directive was applied.

You wouldn’t usually expect your users to enter AngularJS expressions into the application (and I’ll show you a more typical use of $parse shortly), but I wanted to demonstrate how deeply you can dive into the internals of AngularJS and deal with changing expressions and not just changing data values.

The process for using the $parse service is simple—the service object is a function whose sole argument is the expression that will be evaluated and that returns a function that you use when you are ready to perform the evaluation. That is to say that the $parse service doesn’t evaluate expressions itself; it is a factory for functions that do the actual work. Here is the statement from the example in which I use the $parse service object:

var expressionFn = $parse(scope.expr);

I pass the expression—which is whatever the user has entered into the input element in this example—to the $parse function and assign the function that I get back to a variable called expressionFn. I then invoke the function, passing in the scope as the source of the data values for the expression, like this:

var result = expressionFn(scope);

You don’t have to use a scope as the source for the values in the expression, but it is usual to do so. (In the next section I show you how to use the scope and local data for the expression.) The result of invoking the function is the evaluated expression, which in the case of my example is the value of the price property after it has been processed by the currency filter, as shown in the figure.

When you are evaluating expressions that the user has provided, you need to deal with the possibility that the expression is invalid. If you delete a couple of characters from the filter name in the input element so that the expression specifies a nonexistent filter, then you will see a message that indicates that the expression cannot be evaluated. This is because I have caught the exception that arises when trying to parse and evaluate an invalid expression.

You must also be prepared to deal with an undefined result when evaluating the expression, which can happen when the expression refers to nonexistent data values. The AngularJS binding directives automatically display an undefined value as the empty string, but you need to deal with this yourself when working directly with the $parse service. In my example, I display the string No result when the expression evaluates to undefined, as follows:

if (result == undefined) {

result = “No result”;

}

2.1. Providing Local Data

The previous example isn’t the way that the $parse service is usually used because it is rare that you can expect the user to enter expressions to be evaluated. It is much more common to have an expression defined within the application for which the user provides data values. In Listing 19-19, you can see how I have rewritten the contents of the expression.html file for this scenario.

Listing 19-19. Evaluating User Values Against a Fixed Expression in the expressions.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Expressions</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.dataValue = “100.23”;

})

.directive(“evalExpression”, function ($parse) {

var expressionFn = $parse(“total | currency”); return {

scope: {

amount: “=amount”,

tax: “=tax”

}

link: function (scope, element, attrs) {

scope.$watch(“amount”, function (newValue) {

var localData = {

total: Number(newValue)

+ (Number(newValue) * (Number(scope.tax) /100))

}

element.text(expressionFn(scope, localData));

});

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<p><input class=”form-control” ng-model=”dataValue” /></p>

<div>

Result: <span eval-expression amount=”dataValue” tax=”10″></span>

</div>

</div>

</body>

</html>

In this example, I have defined the directive using a definition object (as described in Chapter 16) for variety. The expression is parsed into a function by the $parse service in the directive factory function. I parse the expression only once, and then I invoke the function to evaluate the expression each time the amount property changes.

The expression includes a reference to a total property that does not exist in the scope and that is, in fact, calculated dynamically in the watcher function using two properties bound to an isolated scope, like this:

var localData = {

total: Number(newValue) + (Number(newValue) * (Number(scope.tax) /100))

}

element.text(expressionFn(scope, localData));

The key point to note in these statements is the way that I pass an object containing a total property as an argument to the expression function. This supplements any values that are taken from the scope and provides a value for the total reference in the expression. The effect is that you can enter a value in the input element, and the total value, including a configurable a tax rate, is displayed as the contents of the span element to which the directive was applied, as shown in Figure 19-9.

3. Interpolating Strings

The $interpolate service and its provider, $interpolateProvider, are used to configure the way that AngularJS performs interpolation, which is the process of inserting expressions into strings. The $interpolate service is more flexible than $parse because it can work with strings that contain expressions rather than just expressions themselves. In Listing 19-20, you can see how I have used the $interpolate service in the expressions.html file.

Listing 19-20. Performing Interpolation in the expressions.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Expressions</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.dataValue = “100.23”;

})

.directive(“evalExpression”, function ($interpolate) {

var interpolationFn

= $interpolate(“The total is: {{amount | currency}} (including tax)”);

return {

scope: {

amount: “=amount”,

tax: “=tax”

},

link: function (scope, element, attrs) {

scope.$watch(“amount”, function (newValue) {

var localData = {

total: Number(newValue)

+ (Number(newValue) * (Number(scope.tax) /100))

}

element.text(interpolationFn(scope));

});

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<p><input class=”form-control” ng-model=”dataValue” /></p>

<div>

<span eval-expression amount=”dataValue” tax=”10″></span>

</div>

</div>

</body>

</html>

As the listing shows, using the $interpolate service is similar to using $parse, although there are a couple of important differences. The first—and most obvious—difference is that the $interpolate service can operate on strings that contain non-AngularJS content mixed with inline bindings. In fact, the {{ and }} characters that denote an inline binding are known as the interpolation characters because they are so closely associated with the $interpolate service. The second difference is that you can’t provide a scope and local data to the interpolation function that the $interpolate service creates. Instead, you must ensure that the data values your expression requires are contained within the object you pass to the interpolation function.

3.1. Configuring Interpolation

AngularJS isn’t the only library that uses the {{ and }} characters, and this can be a problem if you are trying to mix AngularJS with another package. Fortunately, you can change the characters that AngularJS uses for interpolation through the provider for the $interpolate service, $interpolateProvider, using the methods described in Table 19-9.

Some care must be taken when using these methods because they will affect all AngularJS interpolation, including inline data bindings in the HTML markup. You can see a demonstration of changing the interpolation characters in Listing 19-21.

Listing 19-21. Changing the Interpolation Characters in the expressions.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Expressions</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.config(function($interpolateProvider) {

$interpolateProvider.startSymbol(“!!”);

$interpolateProvider.endSymbol(“!!”);

});

.controller(“defaultCtrl”, function ($scope) {

$scope.dataValue = “100.23”;

})

.directive(“evalExpression”, function ($interpolate) {

var interpolationFn

= $interpolate(“The total is: !!amount | currency!! (including tax)”);

return {

scope: {

amount: “=amount”,

tax: “=tax”

},

link: function (scope, element, attrs) {

scope.$watch(“amount”, function (newValue) {

var localData = {

total: Number(newValue)

+ (Number(newValue) * (Number(scope.tax) / 100))

}

element.text(interpolationFn(scope));

});

}

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”well”>

<p><input class=”form-control” ng-model=”dataValue” /></p>

<div>

<span eval-expression amount=”dataValue” tax=”10″></span>

<p>Original amount: !!dataValue!!</p>

</div>

</div>

</body>

</html>

I have changed the start and the end symbols to !!. My example application will no longer recognize {{ and }} as denoting an inline binding expression and will operate only on my new character sequence, as follows:

$interpolate(“The total is: !!amount | currency!! (including tax)”);

I added an inline expression to the body section of the expressions.html document to demonstrate that the effect is wider than just the direct use of the $interpolate service:

<p>Original amount: !!dataValue!!</p>

Regular inline bindings are processed by AngularJS using the $interpolate service, and since service objects are singletons, any configuration changes are applied throughout the module.

4. Compiling Content

The $compile service processes an HTML fragment that contains bindings and expressions to create a function that can then be used to generate content from a scope. This is rather like the $parse and $interpolate services, but with support for directives. In Listing 19-22, you can see that using the $compile service is slightly more complex than the other services in this section—but only slightly.

Listing 19-22. Compiling Content in the expressions.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Expressions</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.cities = [“London”, “Paris”, “New York”];

})

.directive(“evalExpression”, function($compile) {

return function (scope, element, attrs) {

var content = “<ul><li ng-repeat=’city in cities’>{{city}}</li></ul>”

var listElem = angular.element(content);

var compileFn = $compile(listElem);

compileFn(scope);

element.append(listElem);

}

});

</script>

</head> <body ng-controller=”defaultCtrl”>

<div class=”well”>

<span eval-expression></span>

</div>

</body>

</html>

The controller in this example defines an array of city names. The directive uses the $compile service to process a fragment of HTML that uses the ng-repeat directive to populate an ul element with the city data. I have broken down the process of using the $compile service into individual statements so I can explain what I am doing step-by-step. First I define a fragment of HTML and wrap it in a jqLite object like this:

var content = “<ul><li ng-repeat=’city in cities’>{{city}}</li></ul>”

var listElem = angular.element(content);

I am working with a simple fragment in this example, but you can pull in more complex content from template elements, just as I demonstrated when working with directives in Chapters 15-17. The next step is to use the $compile service object, which is a function, to create the function that will be used to generate the content:

var compileFn = $compile(listElem);

Once I have the compilation function, I can invoke it to process the content in the fragment. This will cause the expressions and directives that the fragment contains to be evaluated and executed, but notice that there is no return value from invoking the compilation function:

compileFn(scope);

Instead, the processing of the content updates the elements in the jqLite object, which is why I finish by adding those elements to the DOM:

element.append(listElem);

The effect is an ul element that contains one li element for each value in the cities scope array, as shown in Figure 19-10.

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 *