© Adam Freeman 2020
A. FreemanPro Angular 9https://doi.org/10.1007/978-1-4842-5998-6_16

16. Creating Structural Directives

Adam Freeman1 
(1)
London, UK
 
Structural directives change the layout of the HTML document by adding and removing elements. They build on the core features available for attribute directives, described in Chapter 15, with additional support for micro-templates, which are small fragments of contents defined within the templates used by components. You can recognize when a structural directive is being used because its name will be prefixed with an asterisk, such as *ngIf or *ngFor. In this chapter, I explain how structural directives are defined and applied, how they work, and how they respond to changes in the data model. Table 16-1 puts structural directives in context.
Table 16-1.

Putting Structural Directives in Context

Question

Answer

What are they?

Structural directives use micro-templates to add content to the HTML document.

Why are they useful?

Structural directives allow content to be added conditionally based on the result of an expression or for the same content to be repeated for each object in a data source, such as an array.

How are they used?

Structural directives are applied to an ng-template element, which contains the content and bindings that comprise its micro-template. The template class uses objects provided by Angular to control the inclusion of the content or to repeat the content.

Are there any pitfalls or limitations?

Unless care is taken, structural directives can make a lot of unnecessary changes to the HTML document, which can ruin the performance of a web application. It is important to make changes only when they are required, as explained in the “Dealing with Collection-Level Data Changes” section later in the chapter.

Are there any alternatives?

You can use the built-in directives for common tasks, but writing custom structural directives provides the ability to tailor behavior to your application.

Table 16-2 summarizes the chapter.
Table 16-2.

Chapter Summary

Problem

Solution

Listing

Creating a structural directive

Apply the @Directive decorator to a class that receives view container and template constructor parameters

1–6

Creating an iterating structural directive

Define a ForOf input property in a structural directive class and iterate over its value

7–12

Handling data changes in a structural directive

Use a differ to detect changes in the ngDoCheck method

13–19

Querying the content of the host element to which a structural directive has been applied

Use the @ContentChild or @ContentChildren decorator

20–26

Preparing the Example Project

In this chapter, I continue working with the example project that I created in Chapter 11 and have been using since. To prepare for this chapter, I simplified the template to remove the form, leaving only the table, as shown in Listing 16-1. (I’ll add the form back in later in the chapter.)

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/pro-angular-9. See Chapter 1 for how to get help if you have problems running the examples.

<div class="m-2">
  <table class="table table-sm table-bordered table-striped">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tbody class="text-white">
      <tr *ngFor="let item of getProducts(); let i = index"
          [pa-attr]="getProducts().length < 6 ? 'bg-success' : 'bg-warning'"
          [pa-product]="item" (pa-category)="newProduct.category=$event">
        <td>{{i + 1}}</td>
        <td>{{item.name}}</td>
        <td [pa-attr]="item.category == 'Soccer' ? 'bg-info' : null">
          {{item.category}}
        </td>
        <td [pa-attr]="'bg-info'">{{item.price}}</td>
      </tr>
    </tbody>
  </table>
</div>
Listing 16-1.

Simplifying the Template in the template.html File in the src/app Folder

Run the following command in the example folder to start the development tools:
ng serve
Open a new browser window and navigate to http://localhost:4200 to see the content shown in Figure 16-1.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig1_HTML.jpg
Figure 16-1.

Running the example application

Creating a Simple Structural Directive

A good place to start with structural directives is to re-create the functionality provided by the ngIf directive, which is relatively simple, is easy to understand, and provides a good foundation for explaining how structural directives work. I start by making changes to the template and working backwards to write the code that supports it. Listing 16-2 shows the template changes.
<div class="m-2">
    <div class="checkbox">
        <label>
            <input type="checkbox" [(ngModel)]="showTable" />
            Show Table
        </label>
    </div>
    <ng-template [paIf]="showTable">
        <table class="table table-sm table-bordered table-striped">
            <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
            <tr *ngFor="let item of getProducts(); let i = index"
                [pa-attr]="getProducts().length < 6 ? 'bg-success' : 'bg-warning'"
                [pa-product]="item" (pa-category)="newProduct.category=$event">
                <td>{{i + 1}}</td>
                <td>{{item.name}}</td>
                <td [pa-attr]="item.category == 'Soccer' ? 'bg-info' : null">
                    {{item.category}}
                </td>
                <td [pa-attr]="'bg-info'">{{item.price}}</td>
            </tr>
        </table>
    </ng-template>
</div>
Listing 16-2.

Applying a Structural Directive in the template.html File in the src/app Folder

This listing uses the full template syntax, in which the directive is applied to an ng-template element, which contains the content that will be used by the directive. In this case, the ng-template element contains the table element and all its contents, including bindings, directives, and expressions. (There is also a concise syntax, which I use later in the chapter.)

The ng-template element has a standard one-way data binding, which targets a directive called paIf, like this:
...
<ng-template [paIf]="showTable">
...
The expression for this binding uses the value of a property called showTable. This is the same property that is used in the other new binding in the template, which has been applied to a checkbox, as follows:
...
<input type="checkbox" checked="true" [(ngModel)]="showTable" />
...
The objectives in this section are to create a structural directive that will add the contents of the ng-template element to the HTML document when the showTable property is true, which will happen when the checkbox is checked, and to remove the contents of the ng-template element when the showTable property is false, which will happen when the checkbox is unchecked. Listing 16-3 adds the showTable property to the component.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm, FormGroup } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup, ProductFormControl } from "./form.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    formGroup: ProductFormGroup = new ProductFormGroup();
    showTable: boolean = false;
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    addProduct(p: Product) {
        this.model.saveProduct(p);
    }
    formSubmitted: boolean = false;
    submitForm() {
        this.addProduct(this.newProduct);
    }
}
Listing 16-3.

Adding a Property in the component.ts File in the src/app Folder

Implementing the Structural Directive Class

You know from the template what the directive should do. To implement the directive, I added a file called structure.directive.ts in the src/app folder and added the code shown in Listing 16-4.
import {
    Directive, SimpleChange, ViewContainerRef, TemplateRef, Input
} from "@angular/core";
@Directive({
    selector: "[paIf]"
})
export class PaStructureDirective {
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>) { }
    @Input("paIf")
    expressionResult: boolean;
    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        let change = changes["expressionResult"];
        if (!change.isFirstChange() && !change.currentValue) {
            this.container.clear();
        } else if (change.currentValue) {
            this.container.createEmbeddedView(this.template);
        }
    }
}
Listing 16-4.

The Contents of the structure.directive.ts File in the src/app Folder

The selector property of the @Directive decorator is used to match host elements that have the paIf attribute; this corresponds to the template additions that I made in Listing 16-1.

There is an input property called expressionResult, which the directive uses to receive the results of the expression from the template. The directive implements the ngOnChanges method to receive change notifications so it can respond to changes in the data model.

The first indication that this is a structural directive comes from the constructor, which asks Angular to provide parameters using some new types.
...
constructor(private container: ViewContainerRef,
    private template: TemplateRef<Object>) {}
...

The ViewContainerRef object is used to manage the contents of the view container, which is the part of the HTML document where the ng-template element appears and for which the directive is responsible.

As its name suggests, the view container is responsible for managing a collection of views. A view is a region of HTML elements that contains directives, bindings, and expressions, and they are created and managed using the methods and properties provided by the ViewContainerRef class, the most useful of which are described in Table 16-3.
Table 16-3.

Useful ViewContainerRef Methods and Properties

Name

Description

element

This property returns an ElementRef object that represents the container element.

createEmbeddedView(template)

This method uses a template to create a new view. See the text after the table for details. This method also accepts optional arguments for context data (as described in the “Creating Iterating Structural Directives” section) and an index position that specifies where the view should be inserted. The result is a ViewRef object that can be used with the other methods in this table.

clear()

This method removes all the views from the container.

length

This property returns the number of views in the container.

get(index)

This method returns the ViewRef object representing the view at the specified index.

indexOf(view)

This method returns the index of the specified ViewRef object.

insert(view, index)

This method inserts a view at the specified index.

remove(Index)

This method removes and destroys the view at the specified index.

detach(index)

This method detaches the view from the specified index without destroying it so that it can be repositioned with the insert method.

Two of the methods from Table 16-3 are required to re-create the ngIf directive’s functionality: createEmbeddedView to show the ng-template element’s content to the user and clear to remove it again.

The createEmbeddedView method adds a view to the view container. This method’s argument is a TemplateRef object, which represents the content of the ng-template element.

The directive receives the TemplateRef object as one of its constructor arguments, for which Angular will provide a value automatically when creating a new instance of the directive class.

Putting everything together, when Angular processes the template.html file, it discovers the ng-template element and its binding and determines that it needs to create a new instance of the PaStructureDirective class. Angular examines the PaStructureDirective constructor and can see that it needs to provide it with ViewContainerRef and TemplateRef objects.
...
constructor(private container: ViewContainerRef,
    private template: TemplateRef<Object>) {}
...

The ViewContainerRef object represents the place in the HTML document occupied by the ng-template element, and the TemplateRef object represents the ng-template element’s contents. Angular passes these objects to the constructor and creates a new instance of the directive class.

Angular then starts processing the expressions and data bindings. As described in Chapter 15, Angular invokes the ngOnChanges method during initialization (just before the ngOnInit method is invoked) and again whenever the value of the directive’s expression changes.

The PaStructureDirective class’s implementation of the ngOnChanges method uses the SimpleChange object that it receives to show or hide the contents of the ng-template element based on the current value of the expression. When the expression is true, the directive displays the ng-template element’s content by adding them to the container view.
...
this.container.createEmbeddedView(this.template);
...
When the result of the expression is false, the directive clears the view container, which removes the elements from the HTML document.
...
this.container.clear();
...

The directive doesn’t have any insight into the contents of the ng-template element and is responsible only for managing its visibility.

Enabling the Structural Directive

The directive must be enabled in the Angular module before it can be used, as shown in Listing 16-5.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule  } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
@NgModule({
    imports: [BrowserModule, FormsModule, ReactiveFormsModule],
    declarations: [ProductComponent, PaAttrDirective, PaModel, PaStructureDirective],
    bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 16-5.

Enabling the Directive in the app.module.ts File in the src/app Folder

Structural directives are enabled in the same way as attribute directives and are specified in the module’s declarations array.

Once you save the changes, the browser will reload the HTML document, and you can see the effect of the new directive: the table element, which is the content of the ng-template element, will be shown only when the checkbox is checked, as shown in Figure 16-2. (If you don’t see the changes or the table isn’t shown when you check the box, restart the Angular development tools and then reload the browser window.)
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig2_HTML.jpg
Figure 16-2.

Creating a structural directive

Note

The contents of the ng-template element are being destroyed and re-created, not simply hidden and revealed. If you want to show or hide content without removing it from the HTML document, then you can use a style binding to set the display or visibility property.

Using the Concise Structural Directive Syntax

The use of the ng-template element helps illustrate the role of the view container in structural directives. The concise syntax does away with the ng-template element and applies the directive and its expression to the outermost element that it would contain, as shown in Listing 16-6.

Tip

The concise structural directive syntax is intended to be easier to use and read, but it is just a matter of preference as to which syntax you use.

<div class="m-2">
  <div class="checkbox">
    <label>
      <input type="checkbox" [(ngModel)]="showTable" />
      Show Table
    </label>
  </div>
  <table *paIf="showTable"
         class="table table-sm table-bordered table-striped">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tbody class="text-white">
      <tr *ngFor="let item of getProducts(); let i = index"
          [pa-attr]="getProducts().length < 6 ? 'bg-success' : 'bg-warning'"
          [pa-product]="item" (pa-category)="newProduct.category=$event">
        <td>{{i + 1}}</td>
        <td>{{item.name}}</td>
        <td [pa-attr]="item.category == 'Soccer' ? 'bg-info' : null">
          {{item.category}}
        </td>
        <td [pa-attr]="'bg-info'">{{item.price}}</td>
      </tr>
    </tbody>
  </table>
</div>
Listing 16-6.

Using the Concise Structural Directive Syntax in the template.html File in the src/app Folder

The ng-template element has been removed, and the directive has been applied to the table element, like this:
...
<table *paIf="showTable" class="table table-sm table-bordered table-striped">
...

The directive’s name is prefixed with an asterisk (the * character) to tell Angular that this is a structural directive that uses the concise syntax. When Angular parses the template.html file, it discovers the directive and the asterisk and handles the elements as though there were an ng-template element in the document. No changes are required to the directive class to support the concise syntax.

Creating Iterating Structural Directives

Angular provides special support for directives that need to iterate over a data source. The best way to demonstrate this is to re-create another of the built-in directives: ngFor.

To prepare for the new directive, I have removed the ngFor directive from the template.html file, inserted an ng-template element, and applied a new directive attribute and expression, as shown in Listing 16-7.
<div class="m-2">
    <div class="checkbox">
      <label>
        <input type="checkbox" [(ngModel)]="showTable" />
        Show Table
      </label>
    </div>
    <table *paIf="showTable"
            class="table table-sm table-bordered table-striped">
        <thead>
            <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
        </thead>
        <tbody>
            <ng-template [paForOf]="getProducts()" let-item>
                <tr><td colspan="4">{{item.name}}</td></tr>
            </ng-template>
        </tbody>
    </table>
</div>
Listing 16-7.

Preparing for a New Structural Directive in the template.html File in the src/app Folder

The full syntax for iterating structural directives is a little odd. In the listing, the ng-template element has two attributes that are used to apply the directive. The first is a standard binding whose expression obtains the data required by the directive, bound to an attribute called paForOf.
...
<ng-template [paForOf]="getProducts()" let-item>
...

The name of this attribute is important. When using an ng-template element, the name of the data source attribute must end with Of to support the concise syntax, which I will introduce shortly.

The second attribute is used to define the implicit value, which allows the currently processed object to be referred to within the ng-template element as the directive iterates through the data source. Unlike other template variables, the implicit variable isn’t assigned a value, and its purpose is only to define the variable name.
...
<ng-template [paForOf]="getProducts()" let-item>
...
In this example, I have used let-item to tell Angular that I want the implicit value to be assigned to a variable called item, which is then used within a string interpolation binding to display the name property of the current data item.
...
<td colspan="4">{{item.name}}</td>
...
Looking at the ng-template element, you can see that the purpose of the new directive is to iterate through the component’s getProducts method and generate a table row for each of them that displays the name property. To implement this functionality, I created a file called iterator.directive.ts in the src/app folder and defined the directive shown in Listing 16-8.
import { Directive, ViewContainerRef, TemplateRef,
             Input, SimpleChange } from "@angular/core";
@Directive({
    selector: "[paForOf]"
})
export class PaIteratorDirective {
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>) {}
    @Input("paForOf")
    dataSource: any;
    ngOnInit() {
        this.container.clear();
        for (let i = 0; i < this.dataSource.length; i++) {
            this.container.createEmbeddedView(this.template,
                 new PaIteratorContext(this.dataSource[i]));
        }
    }
}
class PaIteratorContext {
    constructor(public $implicit: any) {}
}
Listing 16-8.

The Contents of the iterator.directive.ts File in the src/app Folder

The selector property in the @Directive decorator matches elements with the paForOf attribute, which is also the source of the data for the dataSource input property and which provides the source of objects that will be iterated.

The ngOnInit method will be called once the value of the input property has been set, and the directive empties the view container using the clear method and adds a new view for each object using the createEmbeddedView method.

When calling the createEmbeddedView method, the directive provides two arguments: the TemplateRef object received through the constructor and a context object. The TemplateRef object provides the content to insert into the container, and the context object provides the data for the implicit value, which is specified using a property called $implicit. It is this object, with its $implicit property, that is assigned to the item template variable and that is referred to in the string interpolation binding. To provide templates with the context object in a type-safe way, I defined a class called PaIteratorContext, whose only property is called $implicit.

The ngOnInit method reveals some important aspects of working with view containers. First, a view container can be populated with multiple views—in this case, one view per object in the data source. The ViewContainerRef class provides the functionality required to manage these views once they have been created, as you will see in the sections that follow.

Second, a template can be reused to create multiple views. In this example, the contents of the ng-template element will be used to create identical tr and td elements for each object in the data source. The td element contains a data binding, which is processed by Angular when each view is created and is used to tailor the content to its data object.

Third, the directive has no special knowledge about the data it is working with and no knowledge of the content that is being generated. Angular takes care of providing the directive with the context it needs from the rest of the application, providing the data source through the input property and providing the content for each view through the TemplateRef object.

Enabling the directive requires an addition to the Angular module, as shown in Listing 16-9.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule  } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
@NgModule({
    imports: [BrowserModule, FormsModule, ReactiveFormsModule],
    declarations: [ProductComponent, PaAttrDirective, PaModel,
        PaStructureDirective, PaIteratorDirective],
    bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 16-9.

Adding a Custom Directive in the app.module.ts File in the src/app Folder

The result is that the directive iterates through the objects in its data source and uses the ng-template element’s content to create a view for each of them, providing rows for the table, as shown in Figure 16-3. You will need to check the box to show the table. (If you don’t see the changes, then start the Angular development tools and reload the browser window.)
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig3_HTML.jpg
Figure 16-3.

Creating an iterating structural directive

Providing Additional Context Data

Structural directives can provide templates with additional values to be assigned to template variables and used in bindings. For example, the ngFor directive provides odd, even, first, and last values. Context values are provided through the same object that defines the $implicit property, and in Listing 16-10, I have re-created the same set of values that ngFor provides.
import { Directive, ViewContainerRef, TemplateRef,
             Input, SimpleChange } from "@angular/core";
@Directive({
    selector: "[paForOf]"
})
export class PaIteratorDirective {
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>) {}
    @Input("paForOf")
    dataSource: any;
    ngOnInit() {
        this.container.clear();
        for (let i = 0; i < this.dataSource.length; i++) {
            this.container.createEmbeddedView(this.template,
                 new PaIteratorContext(this.dataSource[i],
                     i, this.dataSource.length));
        }
    }
}
class PaIteratorContext {
    odd: boolean; even: boolean;
    first: boolean; last: boolean;
    constructor(public $implicit: any,
            public index: number, total: number ) {
        this.odd = index % 2 == 1;
        this.even = !this.odd;
        this.first = index == 0;
        this.last = index == total - 1;
    }
}
Listing 16-10.

Providing Context Data in the iterator.directive.ts File in the src/app Folder

This listing defines additional properties in the PaIteratorContext class and expands its constructor so that it receives additional parameters, which are used to set the property values.

The effect of these additions is that context object properties can be used to create template variables, which can then be referred to in binding expressions, as shown in Listing 16-11.
<div class="m-2">
    <div class="checkbox">
      <label>
        <input type="checkbox" [(ngModel)]="showTable" />
        Show Table
      </label>
    </div>
    <table *paIf="showTable"
            class="table table-sm table-bordered table-striped">
        <thead>
            <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
        </thead>
        <tbody>
            <ng-template [paForOf]="getProducts()" let-item let-i="index"
                    let-odd="odd" let-even="even">
                <tr [class.bg-info]="odd" [class.bg-warning]="even">
                    <td>{{i + 1}}</td>
                    <td>{{item.name}}</td>
                    <td>{{item.category}}</td>
                    <td>{{item.price}}</td>
                </tr>
            </ng-template>
        </tbody>
    </table>
</div>
Listing 16-11.

Using Structural Directive Context Data in the template.html File in the src/app Folder

Template variables are created using the let-<name> attribute syntax and assigned one of the context data values. In this listing, I used the odd and even context values to create template variables of the same name, which are then incorporated into class bindings on the tr element, resulting in striped table rows, as shown in Figure 16-4. The listing also adds table cells to display all the Product properties.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig4_HTML.jpg
Figure 16-4.

Using directive context data

Using the Concise Structure Syntax

Iterating structural directives support the concise syntax and omit the ng-template element, as shown in Listing 16-12.
<div class="m-2">
    <div class="checkbox">
      <label>
        <input type="checkbox" [(ngModel)]="showTable" />
        Show Table
      </label>
    </div>
    <table *paIf="showTable"
            class="table table-sm table-bordered table-striped">
        <thead>
            <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
        </thead>
        <tbody>
            <tr *paFor="let item of getProducts(); let i = index; let odd = odd;
                    let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
                <td>{{i + 1}}</td>
                <td>{{item.name}}</td>
                <td>{{item.category}}</td>
                <td>{{item.price}}</td>
            </tr>
        </tbody>
    </table>
</div>
Listing 16-12.

Using the Concise Syntax in the template.html File in the src/app Folder

This is a more substantial change than the one required for attribute directives. The biggest change is in the attribute used to apply the directive. When using the full syntax, the directive was applied to the ng-template element using the attribute specified by its selector, like this:
...
<ng-template [paForOf]="getProducts()" let-item let-i="index" let-odd="odd"
    let-even="even">
...
When using the concise syntax, the Of part of the attribute is omitted, the name is prefixed with an asterisk, and the brackets are omitted.
...
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
            let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
...

The other change is to incorporate all the context values into the directive’s expression, replacing the individual let- attributes. The main data value becomes part of the initial expression, with additional context values separated by semicolons.

No changes are required to the directive to support the concise syntax, whose selector and input property still specify an attribute called paForOf. Angular takes care of expanding the concise syntax, and the directive doesn’t know or care whether an ng-template element has been used.

Dealing with Property-Level Data Changes

There are two kinds of changes that can occur in the data sources used by iterating structural directives. The first kind happens when the properties of an individual object change. This has a knock-on effect on the data bindings contained within the ng-template element, either directly through a change in the implicit value or indirectly through the additional context values provided by the directive. Angular takes care of these changes automatically, reflecting any changes in the context data in the bindings that depend on them.

To demonstrate, in Listing 16-13 I have added a call to the standard JavaScript setInterval function in the constructor of the context class. The function passed to setInterval alters the odd and even properties and changes the value of the price property of the Product object that is used as the implicit value.
...
class PaIteratorContext {
    odd: boolean; even: boolean;
    first: boolean; last: boolean;
    constructor(public $implicit: any,
            public index: number, total: number ) {
        this.odd = index % 2 == 1;
        this.even = !this.odd;
        this.first = index == 0;
        this.last = index == total - 1;
        setInterval(() => {
            this.odd = !this.odd; this.even = !this.even;
            this.$implicit.price++;
        }, 2000);
    }
}
...
Listing 16-13.

Modifying Individual Objects in the iterator.directive.ts File in the src/app Folder

Once every two seconds, the values of the odd and even properties are inverted, and the price value is incremented. When you save the changes, you will see that the colors of the table rows change and the prices slowly increase, as illustrated in Figure 16-5.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig5_HTML.jpg
Figure 16-5.

Automatic change detection for individual data source objects

Dealing with Collection-Level Data Changes

The second type of change occurs when the objects within the collection are added, removed, or replaced. Angular doesn’t detect this kind of change automatically, which means the iterating directive’s ngOnChanges method won’t be invoked.

Receiving notifications about collection-level changes is done by implementing the ngDoCheck method, which is called whenever a data change is detected in the application, regardless of where that change occurs or what kind of change it is. The ngDoCheck method allows a directive to respond to changes even when they are not automatically detected by Angular. Implementing the ngDoCheck method requires caution, however, because it represents a pitfall that can destroy the performance of a web application. To demonstrate the problem, Listing 16-14 implements the ngDoCheck method so that the directive updates the content it displays when there is a change.
import { Directive, ViewContainerRef, TemplateRef,
             Input, SimpleChange } from "@angular/core";
@Directive({
    selector: "[paForOf]"
})
export class PaIteratorDirective {
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>) {}
    @Input("paForOf")
    dataSource: any;
    ngOnInit() {
        this.updateContent();
    }
    ngDoCheck() {
        console.log("ngDoCheck Called");
        this.updateContent();
    }
    private updateContent() {
        this.container.clear();
        for (let i = 0; i < this.dataSource.length; i++) {
            this.container.createEmbeddedView(this.template,
                 new PaIteratorContext(this.dataSource[i],
                     i, this.dataSource.length));
        }
    }
}
class PaIteratorContext {
    odd: boolean; even: boolean;
    first: boolean; last: boolean;
    constructor(public $implicit: any,
            public index: number, total: number ) {
        this.odd = index % 2 == 1;
        this.even = !this.odd;
        this.first = index == 0;
        this.last = index == total - 1;
        // setInterval(() => {
        //     this.odd = !this.odd; this.even = !this.even;
        //     this.$implicit.price++;
        // }, 2000);
    }
}
Listing 16-14.

Implementing the ngDoCheck Methods in the iterator.directive.ts File in the src/app Folder

The ngOnInit and ngDoCheck methods both call a new updateContent method that clears the contents of the view container and generates new template content for each object in the data source. I have also commented out the call to the setInterval function in the PaIteratorContext class.

To understand the problem with collection-level changes and the ngDoCheck method, I need to restore the form to the component’s template, as shown in Listing 16-15.
<div class="row m-2">
    <div class="col-4">
        <form class="m-2" novalidate (ngSubmit)="submitForm()">
            <div class="form-group">
                <label>Name</label>
                <input class="form-control" name="name"
                    [(ngModel)]="newProduct.name" />
            </div>
            <div class="form-group">
                <label>Category</label>
                <input class="form-control" name="category"
                    [(ngModel)]="newProduct.category" />
            </div>
            <div class="form-group">
                <label>Price</label>
                <input class="form-control" name="price"
                    [(ngModel)]="newProduct.price" />
            </div>
            <button class="btn btn-primary" type="submit">Create</button>
        </form>
    </div>
    <div class="col-8">
        <div class="checkbox">
        <label>
            <input type="checkbox" [(ngModel)]="showTable" />
            Show Table
        </label>
        </div>
        <table *paIf="showTable"
                class="table table-sm table-bordered table-striped">
            <thead>
                <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
            </thead>
            <tbody>
                <tr *paFor="let item of getProducts(); let i = index; let odd = odd;
                        let even = even" [class.bg-info]="odd"
                        [class.bg-warning]="even">
                    <td>{{i + 1}}</td>
                    <td>{{item.name}}</td>
                    <td>{{item.category}}</td>
                    <td>{{item.price}}</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
Listing 16-15.

Restoring the HTML Form in the template.html File in the src/app Folder

When you save the changes to the template, the HTML form will be displayed alongside the table of products, as shown in Figure 16-6. (You will have to check the box to show the table.)
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig6_HTML.jpg
Figure 16-6.

Restoring the table in the template

The problem with the ngDoCheck method is that it is invoked every time Angular detects a change anywhere in the application—and those changes happen more often than you might expect.

To demonstrate how often changes occur, I added a call to the console.log method within the directive’s ngDoCheck method in Listing 16-14 so that a message will be displayed in the browser’s JavaScript console each time the ngDoCheck method is called. Use the HTML form to create a new product and see how many messages are written out to the browser’s JavaScript console, each of which represents a change detected by Angular and which results in a call to the ngDoCheck method.

A new message is displayed each time an input element gets the focus, each time a key event is triggered, each time a validation check is performed, and so on. A quick test adding a Running Shoes product in the Running category with a price of 100 generates 27 messages on my system, although the exact number will vary based on how you navigate between elements, whether you need to correct typos, and so on.

For each of those 27 times, the structural directive destroys and re-creates its content, which means producing new tr and td elements with new directive and binding objects.

There are only a few rows of data in the example application, but these are expensive operations, and a real application can grind to a halt as the content is repeatedly destroyed and re-created. The worst part of this problem is that all the changes except one were unnecessary because the content in the table didn’t need to be updated until the new Product object was added to the data model. For all the other changes, the directive destroyed its content and created an identical replacement.

Fortunately, Angular provides some tools for managing updates more efficiently and updating content only when it is required. The ngDoCheck method will still be called for all changes in the application, but the directive can inspect its data to see whether any changes that require new content have occurred, as shown in Listing 16-16.
import { Directive, ViewContainerRef, TemplateRef,
             Input, SimpleChange, IterableDiffer, IterableDiffers,
             ChangeDetectorRef, CollectionChangeRecord, DefaultIterableDiffer
} from "@angular/core";
@Directive({
    selector: "[paForOf]"
})
export class PaIteratorDirective {
    private differ: DefaultIterableDiffer<any>;
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>,
        private differs: IterableDiffers,
        private changeDetector: ChangeDetectorRef) {
    }
    @Input("paForOf")
    dataSource: any;
    ngOnInit() {
        this.differ =
           <DefaultIterableDiffer<any>> this.differs.find(this.dataSource).create();
    }
    ngDoCheck() {
        let changes = this.differ.diff(this.dataSource);
        if (changes != null) {
            console.log("ngDoCheck called, changes detected");
            changes.forEachAddedItem(addition => {
                this.container.createEmbeddedView(this.template,
                     new PaIteratorContext(addition.item,
                         addition.currentIndex, changes.length));
            });
        }
    }
}
class PaIteratorContext {
    odd: boolean; even: boolean;
    first: boolean; last: boolean;
    constructor(public $implicit: any,
            public index: number, total: number ) {
        this.odd = index % 2 == 1;
        this.even = !this.odd;
        this.first = index == 0;
        this.last = index == total - 1;
    }
}
Listing 16-16.

Minimizing Content Changes in the iterator.directive.ts File in the src/app Folder

The idea is to work out whether there have been objects added, removed, or moved from the collection. This means the directive has to do some work every time the ngDoCheck method is called to avoid unnecessary and expensive DOM operations when there are no collection changes to process.

The process starts in the constructor, which receives two new arguments whose values will be provided by Angular when a new instance of the directive class is created. The IterableDiffers and ChangeDetectorRef objects are used to set up change detection on the data source collection in the ngOnInit method, like this:
...
ngOnInit() {
    this.differ =
        <DefaultIterableDiffer<any>> this.differs.find(this.dataSource).create();
}
...

Angular includes built-in classes, known as differs, that can detect changes in different types of objects. The IterableDiffers.find method accepts an object and returns an IterableDifferFactory object that is capable of creating a differ class for that object. The IterableDifferFactory class defines a create method that returns a DefaultIterableDiffer object that will perform the actual change detection, using the ChangeDetectorRef object that was received in the constructor.

The important part of this incantation is the DefaultIterableDiffer object, which was assigned to a property called differ so that it can be used when the ngDoCheck method is called.
...
ngDoCheck() {
    let changes = this.differ.diff(this.dataSource);
    if (changes != null) {
        console.log("ngDoCheck called, changes detected");
        changes.forEachAddedItem(addition => {
            this.container.createEmbeddedView(this.template,
                new PaIteratorContext(addition.item,
                    addition.currentIndex, changes.length));
        });
    }
}
...
The DefaultIterableDiffer.diff method accepts an object for comparison and returns a list of the changes or null if there have been no changes. Checking for the null result allows the directive to avoid unnecessary work when the ngDoCheck method is called for changes elsewhere in the application. The object returned by the diff method provides the properties and methods described in Table 16-4 for processing changes.
Table 16-4.

The DefaultIterableDiffer.Diff Results Methods and Properties

Name

Description

collection

This property returns the collection of objects that has been inspected for changes.

length

This property returns the number of objects in the collection.

forEachItem(func)

This method invokes the specified function for each object in the collection.

forEachPreviousItem(func)

This method invokes the specified function for each object in the previous version of the collection.

forEachAddedItem(func)

This method invokes the specified function for each new object in the collection.

forEachMovedItem(func)

This method invokes the specified function for each object whose position has changed.

forEachRemovedItem(func)

This method invokes the specified function for each object that was removed from the collection.

forEachIdentityChange(func)

This method invokes the specified function for each object whose identity has changed.

The functions that are passed to the methods described in Table 16-4 will receive a CollectionChangeRecord object that describes an item and how it has changed, using the properties shown in Table 16-5.
Table 16-5.

The CollectionChangeRecord Properties

Name

Description

item

This property returns the data item.

trackById

This property returns the identity value if a trackBy function is used.

currentIndex

This property returns the current index of the item in the collection.

previousIndex

This property returns the previous index of the item in the collection.

The code in Listing 16-16 only needs to deal with new objects in the data source since that is the only change that the rest of the application can perform. If the result of the diff method isn’t null, then I use the forEachAddedItem method to invoke a fat arrow function for each new object that has been detected. The function is called once for each new object and uses the properties in Table 16-5 to create new views in the view container.

The changes in Listing 16-16 included a new console message that is written to the browser’s JavaScript console only when there has been a data change detected by the directive. If you repeat the process of adding a new product, you will see that the message is displayed only when the application first starts and when the Create button is clicked. The ngDoCheck method is still being called, and the directive has to check for data changes every time, so there is still unnecessary work going on. But these operations are much less expensive and time-consuming than destroying and then re-creating HTML elements.

Keeping Track of Views

Handling change detection is simple when you are handling the creation of new data items. Other operations—such as dealing with deletions or modifications—are more complex and require the directive to keep track of which view is associated with which data object.

To demonstrate, I am going to add support for deleting a Product object from the data model. First, Listing 16-17 adds a method to the component to delete a product using its key. This isn’t a requirement because the template could access the repository through the component’s model property, but it can help make applications easier to understand when all of the data is accessed and used in the same way.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm, FormGroup } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup, ProductFormControl } from "./form.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    formGroup: ProductFormGroup = new ProductFormGroup();
    showTable: boolean = false;
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    addProduct(p: Product) {
        this.model.saveProduct(p);
    }
    deleteProduct(key: number) {
        this.model.deleteProduct(key);
    }
    formSubmitted: boolean = false;
    submitForm() {
        this.addProduct(this.newProduct);
    }
}
Listing 16-17.

Adding a Delete Method in the component.ts File in the src/app Folder

Listing 16-18 updates the template so that the content generated by the structural directive contains a column of button elements that will delete the data object associated with the row that contains it.
...
<table *paIf="showTable"
        class="table table-sm table-bordered table-striped">
    <thead>
        <tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
    </thead>
    <tbody>
        <tr *paFor="let item of getProducts(); let i = index; let odd = odd;
                let even = even" [class.bg-info]="odd"
                [class.bg-warning]="even">
            <td style="vertical-align:middle">{{i + 1}}</td>
            <td style="vertical-align:middle">{{item.name}}</td>
            <td style="vertical-align:middle">{{item.category}}</td>
            <td style="vertical-align:middle">{{item.price}}</td>
            <td class="text-center">
                <button class="btn btn-danger btn-sm"
                        (click)="deleteProduct(item.id)">
                    Delete
                </button>
            </td>
        </tr>
    </tbody>
</table>
...
Listing 16-18.

Adding a Delete Button in the template.html File in the src/app Folder

The button elements have click event bindings that call the component’s deleteProduct method. I also set the value of the CSS style property vertical-align on the existing td elements so the text in the table is aligned with the button text. The final step is to process the data changes in the structural directive so that it responds when an object is removed from the data source, as shown in Listing 16-19.
import {
    Directive, ViewContainerRef, TemplateRef,
    Input, SimpleChange, IterableDiffer, IterableDiffers,
    ChangeDetectorRef, CollectionChangeRecord, DefaultIterableDiffer, ViewRef
} from "@angular/core";
@Directive({
    selector: "[paForOf]"
})
export class PaIteratorDirective {
    private differ: DefaultIterableDiffer<any>;
    private views: Map<any, PaIteratorContext> = new Map<any, PaIteratorContext>();
    constructor(private container: ViewContainerRef,
        private template: TemplateRef<Object>,
        private differs: IterableDiffers,
        private changeDetector: ChangeDetectorRef) {
    }
    @Input("paForOf")
    dataSource: any;
    ngOnInit() {
        this.differ =
            <DefaultIterableDiffer<any>>this.differs.find(this.dataSource).create();
    }
    ngDoCheck() {
        let changes = this.differ.diff(this.dataSource);
        if (changes != null) {
            changes.forEachAddedItem(addition => {
                let context = new PaIteratorContext(addition.item,
                    addition.currentIndex, changes.length);
                context.view = this.container.createEmbeddedView(this.template,
                    context);
                this.views.set(addition.trackById, context);
            });
            let removals = false;
            changes.forEachRemovedItem(removal => {
                removals = true;
                let context = this.views.get(removal.trackById);
                if (context != null) {
                    this.container.remove(this.container.indexOf(context.view));
                    this.views.delete(removal.trackById);
                }
            });
            if (removals) {
                let index = 0;
                this.views.forEach(context =>
                    context.setData(index++, this.views.size));
            }
        }
    }
}
class PaIteratorContext {
    index: number;
    odd: boolean; even: boolean;
    first: boolean; last: boolean;
    view: ViewRef;
    constructor(public $implicit: any,
            public position: number, total: number ) {
        this.setData(position, total);
    }
    setData(index: number, total: number) {
        this.index = index;
        this.odd = index % 2 == 1;
        this.even = !this.odd;
        this.first = index == 0;
        this.last = index == total - 1;
    }
}
Listing 16-19.

Responding to a Removed Item in the iterator.directive.ts File in the src/app Folder

Two tasks are required to handle removed objects. The first task is updating the set of views by removing the ones that correspond to the items provided by the forEachRemovedItem method. This means keeping track of the mapping between the data objects and the views that represent them, which I have done by adding a ViewRef property to the PaIteratorContext class and using a Map to collect them, indexed by the value of the CollectionChangeRecord.trackById property.

When processing the collection changes, the directive handles each removed object by retrieving the corresponding PaIteratorContext object from the Map, getting its ViewRef object, and passing it to the ViewContainerRef.remove element to remove the content associated with the object from the view container.

The second task is to update the context data for those objects that remain so that the bindings that rely on a view’s position in the view container are updated correctly. The directive calls the PaIteratorContext.setData method for each context object left in the Map to update the view’s position in the container and to update the total number of views that are in use. Without these changes, the properties provided by the context object wouldn’t accurately reflect the data model, which means the background colors for the rows wouldn’t be striped and the Delete buttons wouldn’t target the right objects.

The effect of these changes is that each table row contains a Delete button that removes the corresponding object from the data model, which in turn triggers an update of the table, as shown in Figure 16-7.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig7_HTML.jpg
Figure 16-7.

Removing objects from the data model

Querying the Host Element Content

Directives can query the contents of their host element to access the directives it contains, known as the content children, which allows directives to coordinate themselves to work together.

Tip

Directives can also work together by sharing services, which I describe in Chapter 19.

To demonstrate how content can be queried, I added a file called cellColor.directive.ts to the src/app folder and used it to define the directive shown in Listing 16-20.
import { Directive, HostBinding } from "@angular/core";
@Directive({
    selector: "td"
})
export class PaCellColor {
    @HostBinding("class")
    bgClass: string = "";
    setColor(dark: Boolean) {
        this.bgClass = dark ? "bg-dark" : "";
    }
}
Listing 16-20.

The Contents of the cellColor.directive.ts File in the src/app Folder

The PaCellColor class defines a simple attribute directive that operates on td elements and that binds to the class property of the host element. The setColor method accepts a Boolean parameter that, when the value is true, sets the class property to bg-dark, which is the Bootstrap class for a dark background.

The PaCellColor class will be the directive that is embedded in the host element’s content in this example. The goal is to write another directive that will query its host element to locate the embedded directive and invoke its setColor method. To that end, I added a file called cellColorSwitcher.directive.ts to the src/app folder and used it to define the directive shown in Listing 16-21.
import { Directive, Input, Output, EventEmitter,
         SimpleChange, ContentChild } from "@angular/core";
import { PaCellColor } from "./cellColor.directive";
@Directive({
    selector: "table"
})
export class PaCellColorSwitcher {
    @Input("paCellDarkColor")
    modelProperty: Boolean;
    @ContentChild(PaCellColor)
    contentChild: PaCellColor;
    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        if (this.contentChild != null) {
            this.contentChild.setColor(changes["modelProperty"].currentValue);
        }
    }
}
Listing 16-21.

The Contents of the cellColorSwitcher.directive.ts File in the src/app Folder

The PaCellColorSwitcher class defines a directive that operates on table elements and that defines an input property called paCellDarkColor. The important part of this directive is the contentChild property.
...
@ContentChild(PaCellColor)
contentChild: PaCellColor;
...

The @ContentChild decorator tells Angular that the directive needs to query the host element’s content and assign the first result of the query to the property. The argument to the @ContentChild director is one or more directive classes. In this case, the argument to the @ContentChild decorator is PaCellColor, which tells Angular to locate the first PaCellColor object contained within the host element’s content and assign it to the decorated property.

Tip

You can also query using template variable names, such that @ContentChild("myVariable") will find the first directive that has been assigned to myVariable.

The query result provides the PaCellColorSwitcher directive with access to the child component and allows it to call the setColor method in response to changes to the input property.

Tip

If you want to include the descendants of children in the results, then you can configure the query, like this: @ContentChild(PaCellColor, { descendants: true}).

In Listing 16-22, I altered the checkbox in the template so it uses the ngModel directive to set a variable that is bound to the PaCellColorSwitcher directive’s input property.
...
<div class="col-8">
    <div class="checkbox">
        <label>
            <input type="checkbox" [(ngModel)]="darkColor" />
            Dark Cell Color
        </label>
    </div>
    <table class="table table-sm table-bordered table-striped"
            [paCellDarkColor]="darkColor">
        <thead>
            <tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
        </thead>
        <tbody>
            <tr *paFor="let item of getProducts(); let i = index; let odd = odd;
                    let even = even" [class.bg-info]="odd"
                    [class.bg-warning]="even">
                    <td style="vertical-align:middle">{{i + 1}}</td>
                    <td style="vertical-align:middle">{{item.name}}</td>
                    <td style="vertical-align:middle">{{item.category}}</td>
                    <td style="vertical-align:middle">{{item.price}}</td>
                    <td class="text-center">
                        <button class="btn btn-danger btn-sm"
                                (click)="deleteProduct(item.id)">
                            Delete
                        </button>
                    </td>
            </tr>
        </tbody>
    </table>
</div>
...
Listing 16-22.

Applying the Directives in the template.html File in the src/app Folder

Listing 16-23 adds the darkColor property to the component.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm, FormGroup } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup, ProductFormControl } from "./form.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    formGroup: ProductFormGroup = new ProductFormGroup();
    showTable: boolean = false;
    darkColor: boolean = false;
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    addProduct(p: Product) {
        this.model.saveProduct(p);
    }
    deleteProduct(key: number) {
        this.model.deleteProduct(key);
    }
    formSubmitted: boolean = false;
    submitForm() {
        this.addProduct(this.newProduct);
    }
}
Listing 16-23.

Defining a Property in the component.ts File in the src/app Folder

The final step is to register the new directives with the Angular module’s declarations property, as shown in Listing 16-24.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule  } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
@NgModule({
    imports: [BrowserModule, FormsModule, ReactiveFormsModule],
    declarations: [ProductComponent, PaAttrDirective, PaModel,
        PaStructureDirective, PaIteratorDirective,
        PaCellColor, PaCellColorSwitcher],
    bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 16-24.

Registering New Directives in the app.module.ts File in the src/app Folder

When you save the changes, you will see a new checkbox above the table. When you check the box, the ngModel directive will cause the PaCellColorSwitcher directive’s input property to be updated, which will call the setColor method of the PaCellColor directive object that was found using the @ContentChild decorator. The visual effect is small because only the first PaCellColor directive is affected, which is the cell that displays the number 1, at the top-left corner of the table, as shown in Figure 16-8. (If you don’t see the color change, then restart the Angular development tools and reload the browser.)
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig8_HTML.jpg
Figure 16-8.

Operating on a content child

Querying Multiple Content Children

The @ContentChild decorator finds the first directive object that matches the argument and assigns it to the decorated property. If you want to receive all the directive objects that match the argument, then you can use the @ContentChildren decorator instead, as shown in Listing 16-25.
import { Directive, Input, Output, EventEmitter,
         SimpleChange, ContentChildren, QueryList } from "@angular/core";
import { PaCellColor } from "./cellColor.directive";
@Directive({
    selector: "table"
})
export class PaCellColorSwitcher {
    @Input("paCellDarkColor")
    modelProperty: Boolean;
    @ContentChildren(PaCellColor, {descendants: true})
    contentChildren: QueryList<PaCellColor>;
    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        this.updateContentChildren(changes["modelProperty"].currentValue);
    }
    private updateContentChildren(dark: Boolean) {
        if (this.contentChildren != null && dark != undefined) {
            this.contentChildren.forEach((child, index) => {
                child.setColor(index % 2 ? dark : !dark);
            });
        }
    }
}
Listing 16-25.

Querying Multiple Children in the cellColorSwitcher.directive.ts File in the src/app Folder

When you use the @ContentChildren decorator, the results of the query are provided through a QueryList, which provides access to the directive objects using the methods and properties described in Table 16-6. The descendants configuration property is used to select descendant elements, and without this value, only direct children are selected.
Table 16-6.

The QueryList Members

Name

Description

length

This property returns the number of matched directive objects.

first

This property returns the first matched directive object.

last

This property returns the last matched directive object.

map(function)

This method calls a function for each matched directive object to create a new array, equivalent to the Array.map method.

filter(function)

This method calls a function for each matched directive object to create an array containing the objects for which the function returns true, equivalent to the Array.filter method.

reduce(function)

This method calls a function for each matched directive object to create a single value, equivalent to the Array.reduce method.

forEach(function)

This method calls a function for each matched directive object, equivalent to the Array.forEach method.

some(function)

This method calls a function for each matched directive object and returns true if the function returns true at least once, equivalent to the Array.some method.

changes

This property is used to monitor the results for changes, as described in the upcoming “Receiving Query Change Notifications” section.

In the listing, the directive responds to changes in the input property value by calling the updateContentChildren method, which in turn uses the forEach method on the QueryList and invokes the setColor method on every second directive that has matched the query. Figure 16-9 shows the effect when the checkbox is selected.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig9_HTML.jpg
Figure 16-9.

Operating on multiple content children

Receiving Query Change Notifications

The results of content queries are live, meaning that they are automatically updated to reflect additions, changes, or deletions in the host element’s content. Receiving a notification when there is a change in the query results requires using the Observable interface, which is provided by the Reactive Extensions package, which is added to projects automatically. I explain how Observable objects work in more detail in Chapter 23, but for now, it is enough to know that they are used internally by Angular to manage changes.

In Listing 16-26, I have updated the PaCellColorSwitcher directive so that it receives notifications when the set of content children in the QueryList changes.
import { Directive, Input, Output, EventEmitter,
         SimpleChange, ContentChildren, QueryList } from "@angular/core";
import { PaCellColor } from "./cellColor.directive";
@Directive({
    selector: "table"
})
export class PaCellColorSwitcher {
    @Input("paCellDarkColor")
    modelProperty: Boolean;
    @ContentChildren(PaCellColor, {descendants: true})
    contentChildren: QueryList<PaCellColor>;
    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        this.updateContentChildren(changes["modelProperty"].currentValue);
    }
    ngAfterContentInit() {
        this.contentChildren.changes.subscribe(() => {
            setTimeout(() => this.updateContentChildren(this.modelProperty), 0);
        });
    }
    private updateContentChildren(dark: Boolean) {
        if (this.contentChildren != null && dark != undefined) {
            this.contentChildren.forEach((child, index) => {
                child.setColor(index % 2 ? dark : !dark);
            });
        }
    }
}
Listing 16-26.

Receiving Notifications in the cellColorSwitcher.directive.ts File in the src/app Folder

The value of a content child query property isn’t set until the ngAfterContentInit lifecycle method is invoked, so I use this method to set up the change notification. The QueryList class defines a changes method that returns a Reactive Extensions Observable object, which defines a subscribe method. This method accepts a function that is called when the contents of the QueryList change, meaning that there is some change in the set of directives matched by the argument to the @ContentChildren decorator. The function that I passed to the subscribe method calls the updateContentChildren method to set the colors, but it does so within a call to the setTimeout function, which delays the execution of the method call until after the subscribe callback function has completed. Without the call to setTimeout, Angular will report an error because the directive tries to start a new content update before the existing one has been fully processed. The result of these changes is that the dark coloring is automatically applied to new table cells that are created when the HTML form is used, as shown in Figure 16-10.
../images/421542_4_En_16_Chapter/421542_4_En_16_Fig10_HTML.jpg
Figure 16-10.

Acting on content query change notifications

Summary

In this chapter, I explained how structural directives work by re-creating the functionality of the built-in ngIf and ngFor directives. I explained the use of view containers and templates, described the full and concise syntax for applying structural directives, and showed you how to create a directive that iterates over a collection of data objects and how directives can query the content of their host element. In the next chapter, I introduce components and explain how they differ from directives.

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

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