Using Element and Event Directives in AngularJS: Using the Element Directives

The first set of directives that I describe in this chapter are used to configure and style elements in the Document Object Model (DOM). These directives are useful for managing the way that an application displays content and data and, since this is AngularJS, for using bindings to change the HTML document dynamically when the data model changes. I have listed the element directives in Table 11-2. I describe each of these directives and demonstrate their use in the sections that follow.

1. Showing, Hiding, and Removing Elements

Many of the directives in this category control whether elements are visible to the user, either by hiding them or by removing them completely from the DOM. Listing 11-2 shows the basic techniques for managing element visibility.

Listing 11-2. Managing Element Visibility in the directives.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directives</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.todos = [

{ action: “Get groceries”, complete: false },

{ action: “Call plumber”, complete: false },

{ action: “Buy running shoes”, complete: true },

{ action: “Buy flowers”, complete: false },

{ action: “Call family”, complete: false }];

});

</script>

<style>

td > *:first-child {font-weight: bold}

</style>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

<h3 class=”panel-header”>To Do List</h3>

<div class=”checkbox well”>

<label>

<input type=”checkbox” ng-model=”todos[2].complete” /> Item 3 is complete

</label>

</div>

<table class=”table”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>

<span ng-hide=”item.complete”>(Incomplete)</span>

<span ng-show=”item.complete”>(Done)</span>

</td>

</tr>

</table>

</div>

</body>

</html>

I have used the ng-show and ng-hide directives to control the visibility of span elements in the last cell of each row of the table. This is a contrived example because I could achieve the same effect using an in-data binding, but I need an example that demonstrates a particular problem, as will become clear shortly.

The ng-show and ng-hide directives control element visibility by adding and removing elements from a class called, confusingly, ng-hide. The ng-hide class applies a CSS style that sets the display property to none, removing the element from view. The difference between ng-show and ng-hide is that ng-show hides elements when its expression evaluates to false and ng-hide hides elements when its expression evaluates to true.

This example also contains a check box that sets the complete property of the third to-do item. I added this to demonstrate that ng-show and ng-hide are, like all other directives, wired up to use data bindings and to demonstrate a limitation of the way that these directives work, as illustrated by Figure 11-2.

You will see that the style that I added to Listing 11-2 is applied only when the to-do item is incomplete, even though I specified that the first child of td elements should be bold:

td > *:first-child {font-weight: bold}

The problem is that the ng-show and ng-hide directives leave the elements they manage in the DOM and just hide them from the user. They are not hidden from the browser, as it were, and so position-based CSS selectors like this one will count hidden elements. In situations like these, you can remove, rather than hide, elements from the DOM with the ng-if directive, as shown in Listing 11-3.

Listing 11-3. Using the ng-if Directive in the directives.html File

<td>

<span ng-if=”!item.complete”>(Incomplete)</span>

<span ng-if=”item.complete”>(Done)</span>

</td>

There is no convenient inverted directive that corresponds to ng-if, so I have to take responsibility for negating the value of the data-bound property to create the effect of the ng-hide directive. As Figure 11-3 shows, using the ng-if directive addresses the issue of the CSS style.

1.1. Avoiding Table Striping Problems and Conflicts with ng-repeat

The ng-show, ng-hide, and ng-if directives all have problems when they are applied to the elements that make up tables, which is a shame because new AngularJS developers often try to use the directives to manage the contents displayed by tables.

First, the way that ng-show and ng-hide work means they cannot be easily used in striped tables. This is just a restatement of the problem I showed you earlier, but it bears demonstration because it is such a common cause of confusion. In Listing 11-4, you can see how I have applied the ng-hide directive to the tr element so that only incomplete items are displayed. I have added the table element to the Bootstrap table-striped class to create the striping effect, as described in Chapter 4.

Listing 11-4. Using ng-hide on the Table Rows in the directives.html File

<table class=”table table-striped”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos” ng-hide=”item.complete”>

<td>{{$index + l}}</td>

<td>{{item.action}}</td>

<td>{{item.complete}}</td>

</tr>

</table>

AngularJS will process the directives, but since the elements are hidden and not removed, the result is inconsistent striping, as shown in Figure 11-4. Notice that the coloring for rows has not been applied in rotation.

This may seem like a problem that the ng-if directive can solve, but you can’t use the ng-if directive on the same element as the ng-repeat directive, like this:

<tr ng-repeat=”item in todos” ng-if=”!item.complete”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>{{item.complete}}</td>

</tr>

Both the ng-repeat and ng-if directives rely on a technique called transclusion, which I describe in Chapter 17 but which essentially means that both directives want to modify the child elements and AngularJS doesn’t know how to allow both to do so. If you try to apply both of these directives to an element, you will see an error similar to this one in the JavaScript console:

Error: [$compile:multidir] Multiple directives [ngRepeat, ngIf] asking for transclusion on:

<!– ngRepeat: item in todos –>

This is a rare example where you can’t wire up multiple AngularJS features to solve a problem. But that doesn’t mean the problem can’t be solved—just that it can’t be solved by applying the ng-repeat and ng-if directives together. The answer is to use a filter, which I describe fully in Chapter 14 but which you can see demonstrated in Listing 11-5.

Listing 11-5. Using a Filter to Resolve the Transclusion Problem in the directives.html File

<table class=”table table-striped”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos | filter: {complete: ,false,}”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>{{item.complete}}</td>

</tr>

</table>

This is an example of a filter that uses an object to match properties on the source items. It selects those to-do items whose complete property is false. As Figure 11-5 shows, this creates a result that works with table striping because elements are created only for those objects that pass through the filter. (Filters, like so much else in AngularJS, are linked to the data model and will dynamically reflect changes in the data array.)

2. Managing Classes and CSS

AngularJS provides a set of directives that are used to assign elements to classes and set individual CSS properties. You can see the first two of these directives—ng-class and ng-style—used in Listing 11-6.

Listing 11-6. Using the ng-class and ng-style Directives in the directives.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directives</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.todos = [

{ action: “Get groceries”, complete: false },

{ action: “Call plumber”, complete: false },

{ action: “Buy running shoes”, complete: true },

{ action: “Buy flowers”, complete: false },

{ action: “Call family”, complete: false }];

$scope.buttonNames = [“Red”, “Green”, “Blue”];

$scope.settings = {

Rows: “Red”,

Columns: “Green”

};

});

</script>

<style>

tr.Red { background-color: lightcoral; }

tr.Green { background-color: lightgreen;}

tr.Blue { background-color: lightblue; }

</style>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

<h3 class=”panel-header”>To Do List</h3>

<div class=”row well”>

<div class=”col-xs-6″ ng-repeat=”(key, val) in settings”>

<h4>{{key}}</h4>

<div class=”radio” ng-repeat=”button in buttonNames”>

<label>

<input type=”radio” ng-model=”settings[key]”
value=”{{button}}”>{{button}}

</label>

</div>

</div>

</div>

<table class=”table”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos” ng-class=”settings.Rows”>

<td>{{$index + l}}</td>

<td>{{item.action}}</td>

<td ng-style=”{‘background-color’: settings.Columns}”>

{{item.complete}}

</td>

</tr>

</table>

</div>

</body>

</html>

I have used a fair number of directives just to get to the point in this example where I can meaningfully apply the ng-class and ng-style directives. At the heart of this example is a simple object that I added to the controller scope:

$scope.settings = {

Rows: “Red”,

Columns: “Green”

};

I will use the Rows property to set the background color of the tr elements in the table and the Columns property to set the background color of the Done column. To let the user set these values, I have used the ng-repeat directive to create two sets of radio buttons, laid out using a Bootstrap grid (as described in Chapter 4). I have used the ng-class directive to set the colors for the tr elements, like this:

<tr ng-repeat=”item in todos” ng-class=”settings.Rows”>

The ng-class directive manages the class attribute of an element. In this example, the tr elements will be assigned to a class based on the value of the Rows property, corresponding to one of the CSS styles I defined:

<style>

tr.Red { background-color: lightcoral; }

tr.Green { background-color: lightgreen;}

tr.Blue { background-color: lightblue; }

</style>

Tip You can specify multiple CSS classes using a map object, where the properties refer to the CSS classes and the values are the expressions that control whether the classes are applied. You can see this use of the ng-class directive in Chapter 8, where I used it in the SportsStore administration application.

I use the ng-style property to set CSS properties directly, rather than through a class:

<td ng-style=”{‘background-color’: settings.Columns}”>((item.complete}}</td>

The ng-style directive is configured using an object whose properties correspond to the CSS properties that should be set—in this case, the background-color property, which will be set to the current value of the Columns model property.

Tip It is generally considered poor technique to apply individual CSS properties to elements. When working with static content, styles applied through classes are much easier to work with, not least because a single change to the style definition will be applied wherever the style has been used. The situation is slightly different when using the ng-style directive because the value for the properties is obtained via a data binding and the usual guidance doesn’t apply. My advice is to use classes when you can, but there is no need to avoid using the ng-style directive.

The result is that you can change the background color for the rows and one column in the table through the radio buttons, as shown in Figure 11-6. The effect is the same, but the rows are configured with classes (and the ng-class directive), and the column is configured by setting the CSS property directly via the ng-style directive.

2.1. Assigning Odd and Even Classes

A variation on the ng-class directive is offered by the ng-class-odd and ng-class-even directives, which are used within an ng-repeat directive and apply classes only to odd- or even-numbered elements. This is similar to using the $odd and $even built-in ng-repeat variables I described in Chapter 10. In Listing 11-7, you can see how I have adapted the example to apply classes for alternate rows, creating a dynamically striped table.

Listing 11-7. Using the ng-class-odd and ng-class-even Directives in the directives.html File

<table class=”table”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos” ng-class-even=”settings.Rows”

ng-class-odd=”settings.Columns”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>{{item.complete}}</td>

</tr>

</table>

I have repurposed the existing settings from the previous example. I no longer use the ng-style directive on the td element and have replaced the ng-class directive with ng-class-even and ng-class-odd.

The ng-class-even directive will apply its data binding value to the class attribute of the element to which it is applied to only if the element is even numbered within the ng-repeat directive. Likewise, the ng-class-odd directive modifies its element only when it is odd numbered. By applying these directives on the same element, I can create the classic striped table effect without needing to use Bootstrap, as shown in Figure 11-7.

These directives are not hugely useful if you are using a CSS framework like Bootstrap, but striping tables is such a common activity that almost every JavaScript toolkit has support for some kind of striping.

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 *