Thursday, 24 May 2018

Angular - Built-in directives

Earlier versions of Angular included over seventy built-in directives. The community contributed many more, and countless private directives have been created for internal applications.
You don't need many of those directives in Angular. You can often achieve the same results with the more capable and expressive Angular binding system. Why create a directive to handle a click when you can write a simple binding such as this?
<button (click)="onSave()">Save</button>
You still benefit from directives that simplify complex tasks. Angular still ships with built-in directives; just not as many. You'll write your own directives, just not as many.
This segment reviews some of the most frequently used built-in directives, classified as either attribute directives or structural directives.

Built-in attribute directives:
Attribute directives listen to and modify the behavior of other HTML elements, attributes, properties, and components. They are usually applied to elements as if they were HTML attributes, hence the name.
Many details are covered in the Attribute Directives guide. Many NgModules such as the RouterModule and the FormsModule define their own attribute directives. This section is an introduction to the most commonly used attribute directives:
  • NgClass - add and remove a set of CSS classes.
  • NgStyle - add and remove a set of HTML styles.
  • NgModel - two-way data binding to an HTML form element.

You typically control how elements appear by adding and removing CSS classes dynamically. You can bind to the ngClass to add or remove several classes simultaneously.
class binding is a good way to add or remove a single class.
<!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>
To add or remove many CSS classes at the same time, the NgClass directive may be the better choice.
Try binding ngClass to a key:value control object. Each key of the object is a CSS class name; its value is true if the class should be added, false if it should be removed.
Consider a setCurrentClasses component method that sets a component property, currentClasses with an object that adds or removes three classes based on the true/false state of three other component properties:
currentClasses: {};
setCurrentClasses() {
  // CSS classes: added/removed per current state of component properties
  this.currentClasses =  {
    'saveable': this.canSave,
    'modified': !this.isUnchanged,
    'special':  this.isSpecial
Adding an ngClass property binding to currentClasses sets the element's classes accordingly:
<div [ngClass]="currentClasses">This div is initially saveable, unchanged, and special</div>
It's up to you to call setCurrentClasses(), both initially and when the dependent properties change.

You can set inline styles dynamically, based on the state of the component. With NgStyle you can set many inline styles simultaneously.
style binding is an easy way to set a single style value.
<div [style.font-size]="isSpecial ? 'x-large' : 'smaller'" >
  This div is x-large or smaller.
To set many inline styles at the same time, the NgStyle directive may be the better choice.
Try binding ngStyle to a key:value control object. Each key of the object is a style name; its value is whatever is appropriate for that style.
Consider a setCurrentStyles component method that sets a component property, currentStyles with an object that defines three styles, based on the state of three other component properties:
currentStyles: {};
setCurrentStyles() {
  // CSS styles: set per current state of component properties
  this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
Adding an ngStyle property binding to currentStyles sets the element's styles accordingly:
<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
It's up to you to call setCurrentStyles(), both initially and when the dependent properties change.

NgModel - Two-way binding to form elements with [(ngModel)]:
When developing data entry forms, you often both display a data property and update that property when the user makes changes.
Two-way data binding with the NgModel directive makes that easy. Here's an example:
src/app/app.component.html (NgModel-1)
<input [(ngModel)]="">
FormsModule is required to use ngModel.
Before using the ngModel directive in a two-way data binding, you must import the FormsModule and add it to the NgModule's imports list.
Here's how to import the FormsModule to make [(ngModel)] available.
src/app/app.module.ts (FormsModule import)
import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // <--- JavaScript import from Angular

/* Other imports */

  imports: [
    FormsModule  // <--- import into the NgModule
  /* Other module metadata */
export class AppModule { }
Inside [(ngModel)]: Looking back at the name binding, note that you could have achieved the same result with separate bindings to the <input> element's value property and input event.
<input [value]=""
       (input)="$" >
That's cumbersome. Who can remember which element property to set and which element event emits user changes? How do you extract the currently displayed text from the input box so you can update the data property? Who wants to look that up each time?
That ngModel directive hides these onerous details behind its own ngModel input and ngModelChange output properties.
The ngModel data property sets the element's value property and the ngModelChange event property listens for changes to the element's value.
You can't apply [(ngModel)] to a non-form native element or a third-party custom component until you write a suitable value accessor, a technique that is beyond the scope of this guide.
Separate ngModel bindings is an improvement over binding to the element's native properties. You can do better.
You shouldn't have to mention the data property twice. Angular should be able to capture the component's data property and set it with a single declaration, which it can with the [(ngModel)] syntax:
<input [(ngModel)]="">
Is [(ngModel)] all you need? Is there ever a reason to fall back to its expanded form?
The [(ngModel)] syntax can only set a data-bound property. If you need to do something more or something different, you can write the expanded form.
The following contrived example forces the input value to uppercase:
Here are all variations in action, including the uppercase version:

Built-in structural directives:
Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure, typically by adding, removing, and manipulating the host elements to which they are attached.
This section is an introduction to the common structural directives:
  • NgIf - conditionally add or remove an element from the DOM.
  • NgSwitch - a set of directives that switch among alternative views.
  • NgForOf - repeat a template for each item in a list.

You can add or remove an element from the DOM by applying an NgIf directive to that element (called the host element). Bind the directive to a condition expression like isActive in this example.
<app-hero-detail *ngIf="isActive"></app-hero-detail>
Don't forget the asterisk (*) in front of ngIf.
When the isActive expression returns a truthy value, NgIf adds the HeroDetailComponent to the DOM. When the expression is falsy, NgIf removes the HeroDetailComponent from the DOM, destroying that component and all of its sub-components.

Show/hide is not the same thing:
You can control the visibility of an element with a class or style binding:
<!-- isSpecial is true -->
<div [class.hidden]="!isSpecial">Show with class</div>
<div [class.hidden]="isSpecial">Hide with class</div>

<!-- HeroDetail is in the DOM but hidden -->
<app-hero-detail [class.hidden]="isSpecial"></app-hero-detail>

<div [style.display]="isSpecial ? 'block' : 'none'">Show with style</div>
<div [style.display]="isSpecial ? 'none'  : 'block'">Hide with style</div>
Hiding an element is quite different from removing an element with NgIf.
When you hide an element, that element and all of its descendents remain in the DOM. All components for those elements stay in memory and Angular may continue to check for changes. You could be holding onto considerable computing resources and degrading performance, for something the user can't see.
When NgIf is false, Angular removes the element and its descendents from the DOM. It destroys their components, potentially freeing up substantial resources, resulting in a more responsive user experience.
The show/hide technique is fine for a few elements with few children. You should be wary when hiding large component trees; NgIf may be the safer choice.

Guard against null:
The ngIf directive is often used to guard against null. Show/hide is useless as a guard. Angular will throw an error if a nested expression tries to access a property of null.
Here we see NgIf guarding two <div>s. The currentHero name will appear only when there is a currentHero. The nullHero will never be displayed.
<div *ngIf="currentHero">Hello, {{}}</div>
<div *ngIf="nullHero">Hello, {{}}</div>

NgForOf is a repeater directive — a way to present a list of items. You define a block of HTML that defines how a single item should be displayed. You tell Angular to use that block as a template for rendering each item in the list.
Here is an example of NgForOf applied to a simple <div>:
<div *ngFor="let hero of heroes">{{}}</div>
You can also apply an NgForOf to a component element, as in this example:
<app-hero-detail *ngFor="let hero of heroes" [hero]="hero"></app-hero-detail>
Don't forget the asterisk (*) in front of ngFor.

*ngFor microsyntax:
The string assigned to *ngFor is not a template expression. It's a microsyntax — a little language of its own that Angular interprets. The string "let hero of heroes" means:
Take each hero in the heroes array, store it in the local hero looping variable, and make it available to the templated HTML for each iteration.
Angular translates this instruction into a <ng-template> around the host element, then uses this template repeatedly to create a new set of elements and bindings for each hero in the list.

Template input variables:
The let keyword before hero creates a template input variable called hero. The NgForOf directive iterates over the heroes array returned by the parent component's heroes property and sets hero to the current item from the array during each iteration.
You reference the hero input variable within the NgForOf host element (and within its descendants) to access the hero's properties. Here it is referenced first in an interpolation and then passed in a binding to the hero property of the <hero-detail> component.
<div *ngFor="let hero of heroes">{{}}</div>
<app-hero-detail *ngFor="let hero of heroes" [hero]="hero"></app-hero-detail>

*ngFor with index:
The index property of the NgForOf directive context returns the zero-based index of the item in each iteration. You can capture the index in a template input variable and use it in the template.
The next example captures the index in a variable named i and displays it with the hero name like this.
<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{}}</div>

*ngFor with trackBy:
The NgForOf directive may perform poorly, especially with large lists. A small change to one item, an item removed, or an item added can trigger a cascade of DOM manipulations.
For example, re-querying the server could reset the list with all new hero objects.
Most, if not all, are previously displayed heroes. You know this because the id of each hero hasn't changed. But Angular sees only a fresh list of new object references. It has no choice but to tear down the old DOM elements and insert all new DOM elements.
Angular can avoid this churn with trackBy. Add a method to the component that returns the value NgForOf should track. In this case, that value is the hero's id.
trackByHeroes(index: number, hero: Hero): number { return; }
In the microsyntax expression, set trackBy to this method.
<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
  ({{}}) {{}}
Here is an illustration of the trackBy effect. "Reset heroes" creates new heroes with the same hero.ids. "Change ids" creates new heroes with new hero.ids.
  • With no trackBy, both buttons trigger complete DOM element replacement.
  • With trackBy, only changing the id triggers element replacement.

The NgSwitch directives:
NgSwitch is like the JavaScript switch statement. It can display one element from among several possible elements, based on a switch condition. Angular puts only the selected element into the DOM.
NgSwitch is actually a set of three, cooperating directives: NgSwitchNgSwitchCase, and NgSwitchDefault as seen in this example.
<div [ngSwitch]="currentHero.emotion">
  <app-happy-hero *ngSwitchCase="'happy'" [hero]="currentHero"></app-happy-hero>
  <app-sad-hero *ngSwitchCase="'sad'" [hero]="currentHero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></app-confused-hero>
  <app-unknown-hero *ngSwitchDefault [hero]="currentHero"></app-unknown-hero>
NgSwitch is the controller directive. Bind it to an expression that returns the switch value. The emotion value in this example is a string, but the switch value can be of any type.
Bind to [ngSwitch]. You'll get an error if you try to set *ngSwitch because NgSwitch is an attribute directive, not a structural directive. It changes the behavior of its companion directives. It doesn't touch the DOM directly.
The NgSwitchCase and NgSwitchDefault directives are structural directives because they add or remove elements from the DOM.
The switch directives are particularly useful for adding and removing component elements. This example switches among four "emotional hero" components defined in the hero-switch.components.ts file. Each component has a hero input property which is bound to the currentHero of the parent component.
Switch directives work as well with native elements and web components too. For example, you could replace the <confused-hero> switch case with the following.
<div *ngSwitchCase="'confused'">Are you as confused as {{}}?</div>

No comments:

Post a Comment