Putting the Built-in Directives in Context
Question | Answer |
---|---|
What are they? | The built-in directives described in this chapter are responsible for selectively including content, selecting between fragments of content, and repeating content for each item in an array. There are also directives for setting an element’s styles and class memberships, as described in Chapter 13. |
Why are they useful? | The tasks that can be performed with these directives are the most common and fundamental in web application development, and they provide the foundation for adapting the content shown to the user based on the data in the application. |
How are they used? | The directives are applied to HTML elements in templates. There are examples throughout this chapter (and in the rest of the book). |
Are there any pitfalls or limitations? | The syntax for using the built-in template directives requires you to remember that some of them (including ngIf and ngFor) must be prefixed with an asterisk, while others (including ngClass, ngStyle, and ngSwitch) must be enclosed in square brackets. I explain why this is required in the “Understanding Micro-Template Directives” sidebar, but it is easy to forget and get an unexpected result. |
Are there any alternatives? | You could write your own custom directives—a process that I described in Chapters 15 and 16—but the built-in directives are well-written and comprehensively tested. For most applications, using the built-in directives is preferable, unless they cannot provide exactly the functionality that is required. |
Chapter Summary
Problem | Solution | Listing |
---|---|---|
Conditionally displaying content based on a data binding expression | Use the ngIf directive | 1–3 |
Choosing between different content based on the value of a data binding expression | Use the ngSwitch directive | 4, 5 |
Generating a section of content for each object produced by a data binding expression | Use the ngFor directive | 6–12 |
Repeating a block of content | Use the ngTemplateOutlet directive | 13–14 |
Preventing template errors | Avoid modifying the application state as a side effect of a data binding expression | 15–19 |
Avoiding context errors | Ensure that data binding expressions use only the properties and methods provided by the template’s component | 20–22 |
Preparing the Example Project
This chapter relies on the example project that was created in Chapter 11 and modified in Chapter 12. To prepare for the topic of this chapter, Listing 13-1 shows changes to the component class that remove features that are no longer required and adds new methods and a property.
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.
Changes in the component.ts File in the src/app Folder
The Contents of the template.html File in the src/app Folder
Using the Built-in Directives
The Built-in Directives
Example | Description |
---|---|
<div *ngIf="expr"></div> | The ngIf directive is used to include an element and its content in the HTML document if the expression evaluates as true. The asterisk before the directive name indicates that this is a micro-template directive, as described in the “Understanding Micro-Template Directives” sidebar. |
<div [ngSwitch]="expr"> <span *ngSwitchCase="expr"></span> <span *ngSwitchDefault></span></div> | The ngSwitch directive is used to choose between multiple elements to include in the HTML document based on the result of an expression, which is then compared to the result of the individual expressions defined using ngSwitchCase directives. If none of the ngSwitchCase values matches, then the element to which the ngSwitchDefault directive has been applied will be used. The asterisks before the ngSwitchCase and ngSwitchDefault directives indicate they are micro-template directives, as described in the “Understanding Micro-Template Directives” sidebar. |
<div *ngFor="#item of expr"></div> | The ngFor directive is used to generate the same set of elements for each object in an array. The asterisk before the directive name indicates that this is a micro-template directive, as described in the “Understanding Micro-Template Directives” sidebar. |
<ng-template [ngTemplateOutlet]="myTempl"></ngtemplate> | The ngTemplateOutlet directive is used to repeat a block of content in a template. |
<div ngClass="expr"></div> | The ngClass directive is used to manage class membership, as described in Chapter 12. |
<div ngStyle="expr"></div> | The ngStyle directive is used to manage styles applied directly to elements (as opposed to applying styles through classes), as described in Chapter 12. |
Using the ngIf Directive
Using the ngIf Directive in the template.html File in the src/app Folder
The ngIf directive has been applied to two div elements, with expressions that check the number of Product objects in the model and whether the name of the first Product is Kayak.
The first expression evaluates as true, which means that div element and its content will be included in the HTML document; the second expression evaluates as false, which means that the second div element will be excluded. Figure 13-2 shows the result.
The ngIf directive adds and removes elements from the HTML document, rather than just showing or hiding them. Use the property or style bindings, described in Chapter 12, if you want to leave elements in place and control their visibility, either by setting the hidden element property to true or by setting the display style property to none.
Some directives, such as ngFor, ngIf, and the nested directives used with ngSwitch, are prefixed with an asterisk, as in *ngFor, *ngIf, and *ngSwitch. The asterisk is shorthand for using directives that rely on content provided as part of the template, known as a micro-template. Directives that use micro-templates are known as structural directives, a description that I revisit in Chapter 16 when I show you how to create them.
You can use either syntax in your templates, but if you use the compact syntax, then you must remember to use the asterisk. I explain how to create your own micro-template directives in Chapter 14.
Using the ngSwitch Directive
Using the ngSwitch Directive in the template.html File in the src/app Folder
The ngSwitchCase directive is used to specify an expression result. If the ngSwitch expression evaluates to the specified result, then that element and its contents will be included in the HTML document. If the expression doesn’t evaluate to the specified result, then the element and its contents will be excluded from the HTML document.
The ngSwitchDefault directive is applied to a fallback element—equivalent to the default label in a JavaScript switch statement—which is included in the HTML document if the expression result doesn’t match any of the results specified by the ngSwitchCase directives.
These statements remove the first item from the model and force Angular to run the change detection process. Neither of the results for the two ngSwitchCase directives matches the result from the getProductCount expression, so the ngSwitchDefault element is included in the HTML document, as shown on the right of Figure 13-4.
Avoiding Literal Value Problems
Component and String Literal Values in the template.html File in the src/app Folder
The values assigned to the ngSwitchCase directives are also expressions, which means you can invoke methods, perform simple inline operations, and read property values, just as you would for the basic data bindings.
Using the ngFor Directive
Using the ngFor Directive in the template.html File in the src/app Folder
The asterisk before the name is required because the directive is using a micro-template, as described in the “Understanding Micro-Template Directives” sidebar. This will make more sense as you become familiar with Angular, but at first, you just have to remember that this directive requires an asterisk (or, as I often do, forget until you see an error displayed in the browser’s JavaScript console and then remember).
This example specifies the component’s getProducts method as the source of data, which allows content to be for each of the Product objects in the model. The right-hand side is an expression in its own right, which means you can prepare data or perform simple manipulation operations within the template.
Put together, the directive in the example tells Angular to enumerate the objects returned by the component’s getProducts method, assign each of them to a variable called item, and then generate a tr element and its td children, evaluating the template expressions they contain.
Using Other Template Variables
The ngFor Local Template Values
Name | Description |
---|---|
index | This number value is assigned to the position of the current object. |
odd | This boolean value returns true if the current object has an odd-numbered position in the data source. |
even | This boolean value returns true if the current object has an even-numbered position in the data source. |
first | This boolean value returns true if the current object is the first one in the data source. |
last | This boolean value returns true if the current object is the last one in the data source. |
Using the Index Value
Using the Index Value in the template.html File in the src/app Folder
Using the Odd and Even Values
Using the odd Value in the template.html File in the src/app Folder
You can see that the ng-template element defines the variables, using the somewhat awkward let-<name> attributes, which are then accessed by the tr and td elements within it. As with so much in Angular, what appears to happen by magic turns out to be straightforward once you understand what is going on behind the scenes, and I explain these features in detail in Chapter 16. A good reason to use the *ngFor syntax is that it provides a more elegant way to express the directive expression, especially when there are multiple template variables.
Using the First and Last Values
Using the first and last Values in the template.html File in the src/app Folder
Minimizing Element Operations
Replacing an Object in the repository.model.ts File in the src/app Folder
The swapProduct method removes the first object from the array and adds a new object that has the same values for the id, name, category, and price properties. This is an example of data values being represented by a new object.
When the ngFor directive examines its data source, it sees it has two operations to perform to reflect the change to the data. The first operation is to destroy the HTML elements that represent the first object in the array. The second operation is to create a new set of HTML elements to represent the new object at the end of the array.
Angular has no way of knowing that the data objects it is dealing with have the same values and that it could perform its work more efficiently by simply moving the existing elements within the HTML document.
This problem affects only two elements in this example, but the problem is much more severe when the data in the application is refreshed from an external data source using Ajax, where all the data model objects can be replaced each time a response is received. Since it is not aware that there have been few real changes, the ngFor directive has to destroy all of its HTML elements and re-create them, which can be an expensive and time-consuming operation.
Adding the Object Comparison Method in the component.ts File in the src/app Folder
The method has to define two parameters: the position of the object in the data source and the data object. The result of the method uniquely identifies an object, and two objects are considered to be equal if they produce the same result.
Providing an Equality Method in the template.html File in the src/app Folder
With this change, the ngFor directive will know that the Product that is removed from the array using the swapProduct method defined in Listing 13-12 is equivalent to the one that is added to the array, even though they are different objects. Rather than delete and create elements, the existing elements can be moved, which is a much simpler and quicker task to perform.
Changes can still be made to the elements—such as by the ngIf directive, which will remove one of the td elements because the new object will be the last item in the data source, but even this is faster than treating the objects separately.
Checking whether the equality method has an effect is a little tricky. The best way that I have found requires using the browser’s F12 developer tools, in this case using the Chrome browser.
Once the application has loaded, right-click the td element that contains the word Kayak in the browser window and select Inspect from the pop-up menu. This will open the Developer Tools window and show the Elements panel.
If the element has been destroyed, then there won’t be an element whose id attribute is old, and the browser will display the word undefined.
Using the ngTemplateOutlet Directive
Using the ngTemplateOutlet Directive in the template.html File in the src/app Folder
Providing Context Data
Providing Context Data in the template.html File in the src/app Folder
Understanding One-Way Data Binding Restrictions
Although the expressions used in one-way data binding and directives look like JavaScript code, you can’t use all the JavaScript—or TypeScript—language features. I explain the restrictions and the reasons for them in the sections that follow.
Using Idempotent Expressions
One-way data bindings must be idempotent, meaning that they can be evaluated repeatedly without changing the state of the application. To demonstrate why, I added a debugging statement to the component’s getProductCount method, as shown in Listing 13-15.
Angular does support modifying the application state, but it must be done using the techniques I describe in Chapter 14.
Adding a Statement in the component.ts File in the src/app Folder
Adding a Property in the component.ts File in the src/app Folder
Adding a Binding in the template.html File in the src/app Folder
Modifying Data in the component.ts File in the src/app Folder
Binding to a Property in the template.html File in the src/app Folder
Understanding the Expression Context
When Angular processes these expressions, the component provides the getProductCount method, which Angular invokes with the specified arguments and then incorporates the result into the HTML document. The component is said to provide the template’s expression context.
The expression context means you can’t access objects defined outside of the template’s component, and in particular, templates can’t access the global namespace. The global namespace is used to define common utilities, such as the console object, which defines the log method I have been using to write out debugging information to the browser’s JavaScript console. The global namespace also includes the Math object, which provides access to some useful arithmetical methods, such as min and max.
Accessing the Global Namespace in the template.html File in the src/app Folder
The error message doesn’t specifically mention the global namespace. Instead, Angular has tried to evaluate the expression using the component as the context and failed to find a Math property.
Defining a Method in the component.ts File in the src/app Folder
Access Global Namespace Functionality in the template.html File in the src/app Folder
Summary
In this chapter, I explained how to use the built-in template directives. I showed you how to select content with the ngIf and ngSwitch directives and how to repeat content using the ngFor directive. I explained why some directive names are prefixed with an asterisk and described the limitations that are placed on template expressions used with these directives and with one-way data bindings in general. In the next chapter, I describe how data bindings are used for events and form elements.