© Venkata Keerti Kotaru 2020
V. K. KotaruAngular for Material Designhttps://doi.org/10.1007/978-1-4842-5434-9_4

4. Angular: Components

Venkata Keerti Kotaru1 
(1)
Hyderabad, India
 

Components are the building blocks of an Angular application. Components bring reusability to a view. Imagine a view that presents user profiles and order information in the sample application that we are building (a superhero profile). We might need to present the same data in many places. Instead of duplicating the code in each instance, let’s create a component and reuse it.

This chapter introduces Angular components. It covers creating new components. An Angular application needs metadata that describes a component. Metadata defines how we use the component in the application. This chapter elaborates the metadata elements, which are typically coded as arguments to the TypeScript decorators on top of the component class.

This chapter also explains how to provide input data to a component and how to receive output from a component. The chapter concludes by discussing lifecycle hooks for a component.

Introduction

In an Angular application, the view is built with a component tree. In a typical HTML page, DOM (Document Object Model) nodes are organized as a tree. It starts with a root node, which has many child nodes. Each child node may have its own child nodes. Listing 4-1 is a small form capturing user input. It has a root div element that has two child nodes—a label and another div, which have more child nodes.
<div>
  <label for="heroName">
    <strong>Hero</strong>
    <input type="text" name="heroName"/>
  </label>
  <div>
    <strong> Enter the hero name </strong>
  </div>
</div>
Listing 4-1

DOM Tree

../images/475625_1_En_4_Chapter/475625_1_En_4_Fig1_HTML.jpg
Figure 4-1

DOM tree

Figure 4-1 is a visual representation.

An Angular component is a custom element—a custom DOM node. It may intermix with HTML nodes. Consider the superhero profile component. It shows a hero’s name, superpowers, and other information. Listing 4-2 has the superhero-profile among the HTML elements (see line 2). The superhero-profile component may contain more elements and child components.
1. <div class="form-group">
2.  <superhero-profile></superhero-profile>
3.  <strong>Hero</strong>
4.    <input type="text" name="heroName"/>
5.  </label>
6.  <div>
7.    <strong> Enter the hero name </strong>
8.  </div>
9. </div>
Listing 4-2

Angular HTML Template with a Component Among Elements

Now, let’s focus on the superhero profile component. Figure 4-2 shows a profile component (or the view) with a title, description, and superpowers, which can be reused throughout the application.
../images/475625_1_En_4_Chapter/475625_1_En_4_Fig2_HTML.jpg
Figure 4-2

Superhero profile component

Create a Component

Let’s begin by creating a new superhero-profile component using Angular CLI. Run the command shown in Listing 4-3 to create a new component.
ng generate component superheroes-material-design/superhero-profile
Listing 4-3

Angular CLI Command for Creating a New Component

Note

The path specified, superheroes-material-design/<component-name>, creates the component under the superheroes-material-design folder. It also looks for a module with the name SuperheroesMaterialDesign.module. Without this path reference, the component is created in the root module.

The component is declared in the module (see Listing 4-4).
@NgModule({
 /∗ For brevity removed unrelated blocks of code ∗/
  imports: [ ],
  declarations: [
    SuperheroProfileComponent
  ]
 /∗ For brevity removed unrelated blocks of code ∗/
})
export class AppModule { }
Listing 4-4

Angular Module with the New Component Declaration

It also creates the following files in a new folder.
  • A TypeScript class file component

  • A template HTML file

  • A style sheet/CSS file

  • A unit test file

Let’s look at the new component’s TypeScript class file. In Listing 4-5, the @Component decorator is on top of the class file. This makes a TypeScript class an Angular component. The decorator has a JSON object as an argument. The fields in this object define the metadata for the component. It defines various aspects of the component, such as the name of the component in the HTML markup, references to the HTML template file, references to the CSS file, and so forth.
@Component({
 selector: 'superhero-profile',
 templateUrl: './superhero-profile.component.html',
 styleUrls: ['./superhero-profile.component.css']
})
export class SuperheroProfileComponent implements OnInit {
 constructor() { }
 ngOnInit() { }
}
Listing 4-5

A Component Class in TypeScript

The following are the metadata attributes frequently used with components.

selector

We refer to a component using the selector value. In this example, we provided a value: superhero-profile. To use this component in another component in the application, the template (HTML file) would create a <superhero-profile></superhero-profile> element (see Listing 4-5).

Note

This attribute is inherited from another decorator, Directive. Components are a type of directive. Directives are discussed later.

templateUrl

A template defines the view of a component. It is the HTML intermixed with Angular bindings and syntaxes. templateUrl refers to the component’s HTML template. Angular CLI places the template in the same directory as the TypeScript file component.

Listing 4-6 is the superhero profile component’s HTML template. The result is shown in Figure 4-3.
<mat-card>
  <h1>Spiderman</h1>
  <p>
    Lives in New York. He is a friendly neighbourhood superhero.
  </p>
  <div>
    <strong>Superpowers</strong>
    <li>
      Spiderman can shoot web
    </li>
    <li>
      He can fly with the help of the web.
    </li>
    <li>
      He is strong
    </li>
  </div>
</mat-card>
Listing 4-6

HTML Template for Sample Superhero Profile

template

Typically, the HTML template code is separated in another file; however, if you prefer to keep the template and the TypeScript code in the same file, use the template attribute (instead of templateUrl). It might be a good idea to use the template attribute for a small number of lines of code (perhaps four to six lines of template code); otherwise, it could be convoluted to use template and the TypeScript in the same file.

styleUrls

Style sheets, or CSS, manage a component’s look and feel. It takes care of colors, fonts, sizes, the margin width and height, and so forth. Styles are preferably separated into one or more files. Specify the relative path of the styles here.

style

The style attribute is used for inline styles; it is in the same file as the TypeScript component.

encapsulation

Traditionally, it is difficult to scope CSS in a web page. Over time, an application’s complexity increases, and a lot of presentation and styling code accumulates. Web pages import styles at the HTML page level. Considering there is a single HTML page that loads at the beginning of an SPA (single-page application), it might mean the style sheet file is used by the entire application. With multiple developers on any typical team, it is very easy to step on one another’s toes. Changing the style in one functionality could adversely affect rest of the application.

The encapsulation attribute helps scope CSS for Angular components. Angular provides three options.
  • emulated: The default option. Angular ensures that the CSS used in a component does not interfere with other components. This is done by adding a surrogate ID to the host element. This host element is added to all CSS selectors that use the styles and elements.

  • shadowDom: A new web standard popularized by Web Components. It helps scope the styles for natively supported components in a web page. This option works well for all browsers that support Web Components and the standard.

  • none: Styles are global.

providers

The providers attribute accepts an array of tokens. With DI (dependency injection) in Angular, the injector uses provider to create an instance and supply the object to the decorated class.

Let’s simplify this explanation. The decorated class is the component. It is a TypeScript class. Components in Angular typically use a reusable class instance (Angular services). It needs to be “provided”—that is, provide metadata for the injector to create an instance. Once the injector creates an instance, it is readily used by the component.

Note that “providers” is a field in the ngModule decorator mentioned in the Chapter 3. An Angular service (reusable TypeScript class) can be provided at the root, module, or component level.

A TypeScript class can maintain the state. The state is a class level variable or an object with data. If we provide at the root level, the state is shared across the application. It acts as a singleton for the application. If we provide it at the module level, the state is shared within the module, which means that it will provide a single instance throughout the module.

If it is provided at the component level, it acts like a local instance and does not share the value with any components; however, the instance is shared in the child components.

Note

Angular 1.x has services and factories. The state of a service or a factory is maintained across the application. The object and data is shared among controllers (in Angular 1.x). Angular 2+ depends on the provider. By default, the TypeScript classes that act as services are transient. Providers define how data is shared in the application.

Services providing dependency injection is discussed more in Chapter 7.

viewProviders

viewProviders are very similar to providers but have one major difference: they do not provide for projected content. The projected content is the DOM and the components supplied within the component’s opening and closing elements (see Listing 4-7).
<my-component>
  <!-- projected content goes here -->
  <div>
    <child-component></child-component>
  </div>
</my-component>
Listing 4-7

Projected Content

This is useful for allowing the contents of a component to be supplied “on the fly.” A component can apply its functionality on any content projected between the opening and closing elements. A component library might extensively use content projection. The downside, however, is that the author of the component does not have control over which content is projected. It is coded by the component’s consumers.

If you need to restrict a provided class to be used by only the real child components and not by the projected content, you may use viewProviders.

Note

Use <ng-content> (a built-in directive in Angular) for projecting content.

An Example to Showcase Projecting Content in Angular

Before we go over using viewProviders, we want to discuss projected content by using an example. Let’s review the component tree that we have built so far. At the root of the application, there is the AppComponent. The AppComponent’s template uses the SuperheroProfile component. To better understand the projected content, let’s create a child component for SuperheroProfile. Let’s name it SuperheroProfileFooter. We will include it in the SuperheroProfile template. Figure 4-3 is a visualization of a component tree.
../images/475625_1_En_4_Chapter/475625_1_En_4_Fig3_HTML.jpg
Figure 4-3

Component tree

Listing 4-8 is the HTML template for the superhero-profile component. It uses the superhero-profile-footer component directly as a child component.
<mat-card class="custom-style">
 <!—Superhero details go here
 <superhero-profile-footer></superhero-profile-footer>
</mat-card>
Listing 4-8

superhero-profile-footer in SuperheroProfile Component Template

On the other hand, we may let the superhero-profile consumers decide what goes into the footer. To do this, we may need to use the footer projected, instead of always using the same child component (hard coded in the component, as in Listing 4-9). Use ng-content in place of the footer component (see line number 4).
1. <mat-card class="custom-style">
2.  <div>Superhero profile.</div>
3.  <!-- Superhero details go here -->
4.  <ng-content></ng-content>
5. </mat-card>
Listing 4-9

SuperheroProfile Template with Support to Projected Content

While using the SuperheroProfile component, supply the footer component. ng-content projects (replaces) child components supplied by the user of the superhero-profile. Listing 4-10 is the code in the AppComponent HTML template.
<app-superhero-profile>
 <!-- projected content goes here -->
 <superhero-profile-footer></superhero-profile-footer>
</app-superhero-profile>
Listing 4-10

SuperheroProfile with Projected Content

In this example, both approaches result in the same view because we are using the same child component and projected content: superhero-profile-footer. The “A superhero made in Hollywood!” text is from the footer (see Figure 4-4); however, the later approach (content projection) allows any component or DOM element to be used as the footer; it does not need to be superheroProfileFooter. The footer is supplied to the component externally.

Note

For simplicity, this section does not describe the features of <mat-card>. Positioning content at the bottom of the card makes it look like a footer; however, in a future chapter, I discuss a better way to use a Material Design card.

../images/475625_1_En_4_Chapter/475625_1_En_4_Fig4_HTML.jpg
Figure 4-4

A view with SuperheroProfile and SuperheroProfileFooter components put together

Using viewProvider

Imagine that the contents of the child component, superhero-profile-footer, are returned by a reusable TypeScript class. Assume that it is a single place for all the superhero data. Eventually, we may use a remote service to get this data from a database. For now, consider Listing 4-11, which returns a string from the class variable.
export default class SuperheroText {
   private _footerText: string = "A superhero, made in Hollywood";
   get footerText(): string {
       return this._footerText;
   }
}
Listing 4-11

A Reusable Class Providing Contents for the Superhero Components

If you do not want this service to be used by the projected content, but to be used only by the direct child component, provide it with viewProviders. In Listing 4-12, SuperheroText is provided with viewProviders (line 6), but it is not injected (in line 9, the constructor does not inject anything).
1. import SuperheroText from '../utilities/superhero-text';
2. @Component({
3. selector: 'app-superhero-profile',
4. templateUrl: './superhero-profile.component.html',
5. styleUrls: ['./superhero-profile.component.css'],
6. viewProviders: [ SuperheroText ]
7. })
8. export class SuperheroProfileComponent {
9. constructor() {
10.  }
11. }
Listing 4-12

SuperheroText Provided at Parent Component

SuperheroProfile’s child components (i.e., SuperheroProfileFooter) inject SuperheroText (see Listing 4-13).
import SuperheroText from '../utilities/superhero-text';
@Component({
 selector: 'superhero-profile-footer',
 templateUrl: './superhero-profile-footer.component.html'
})
export class SuperheroProfileFooterComponent {
 constructor(private superheroText: SuperheroText) { }
}
Listing 4-13

A Child Component Injecting a Class Provided by Parent

The superheroText object used by the direct child component referenced in the profile component (viewProvider) works without an error (see Listing 4-14).
---- superhero-profile.component.html ---
<mat-card class="custom-style">
 <!-- Superhero details go here -->
 <superhero-profile-footer></superhero-profile-footer>
</mat-card>
Listing 4-14

Direct Child Component Using superheroText Works Well

If superhero-profile-footer is projected, and it attempts to inject superheroText, it will return the following error. Hence, it gives control to the author or the superhero-profile component to disallow injecting a service by a projected component.
NullInjectorError: No provider for SuperheroText!

Input to a Component

Components are building blocks of an Angular application. They are reusable blocks of code. A component is expected to be designed with a single responsibility principle in mind. A component should be responsible for a particular view in the application.

So far, we have built a component to show a superhero profile. The same component could be used for any superhero in the system. This means, it expects input. The component processes and presents the data in a uniform manner.

To accept input into a component, Angular follows a natural HTML way: element attributes. As you might have noticed, a div tag can accept additional input, like a CSS class, with a class attribute. It accepts a unique identifier with an id attribute. Similarly, a text field accepts text with a value attribute, and so on.

While building custom elements with Angular, there could be one or more attributes added. The TypeScript code has a reference to the variable, and you can also use it in a template for presentation.

Use the @Input decorator to qualify a Component class field as an input. Import the Input decorator for the Angular core library (see Listing 4-15).
import { Component, OnInit, Input, OnChanges } from '@angular/core';
export class SuperheroProfileComponent {
 @Input() name: string;
 @Input() firstAppearance: string;
 @Input("lives-in") livesIn: string;
}
Listing 4-15

Component Class with Input Elements

Notice the input to the decorator in the livesIn field. It allows you to rename the component element attribute. In this case, we can use livesIn as lives-in in the template.

Provide input while invoking the component in the HTML template (see Listing 4-16).
<app-superhero-profile name="Chhota Bheem" firstAppearance="2008" lives-in="India">
Listing 4-16

Input in the Template

Validate Input with a Setter

We can validate the input if we use the @Input decorator on a setter function instead of a class field. In Listing 4-17, we validate that the first appearance is greater than 1950. Decorate the setter with @Input. Notice that we used a private field in the component class. Getter and a setter allow you to access the private field and set a value to it through input.
 // create a private field
 private _firstAppearance: number;
 // use a public setter
 @Input()
 set firstAppearance(val: number) {
   if (val > 1950){
     this._firstAppearance = val;
   } else {
     console.error("Superhero is too old");
   }
 }
   // getter for the use of template.
   get firstAppearance() {
     return this._firstAppearance;
   }
Listing 4-17

Use a Setter to Validate Input

Validate Input with the ngOnChanges Lifecycle Hook

Setters are effective for validating input when the number of input attributes is small. The downside to this approach is that we need a separate function for each attribute. We can combine all validations to a single function with ngOnChanges.

To use ngOnChanges, import and implement the onChanges interface. Override the ngOnChanges function (see Listing 4-18). Notice the SimpleChanges parameter. The object will have the previous value and a new value in each field.

Lifecycle hooks are discussed later in the chapter.
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
export class SuperheroProfileComponent implements OnChanges {
 ngOnChanges(changes: SimpleChanges) {
   console.log(changes);
 }
}
Listing 4-18

ngOnChanges Override

../images/475625_1_En_4_Chapter/475625_1_En_4_Fig5_HTML.jpg
Figure 4-5

SimpleChange object with values

Output from a Component

A component can emit events, which can be tapped into by a consuming component. To demonstrate an output event, consider enhancing the Superhero Profile component. Let’s add a button that emits the complete superhero object to the consuming component.

The component using SuperheroProfile will add an event handler to access the data. The event in the example is named flyWithSuperhero. In Listing 4-19, the event is passed to the handler function. The object received from the superhero profile component as an output is accessed in the app component.
--- app.component.html ---
<app-superhero-profile (flyWithSuperhero)="printSuperhero($event)" name="Chhota Bheem" firstAppearance="2008" lives-in="India">
--- app.component.ts ---
 printSuperhero(hero: Superhero) {
   console.log(hero); // print the received event object.
 }
Listing 4-19

AppComponent Accessing the flyWithSuperhero New Event

Let’s create a Superhero data type for the SuperheroProfile component to help define a strict type to the output object (see Listing 4-20).
--- superhero-profile.component.ts ---
// Create a type representing superhero
export type Superhero = {
 name: string;
 firstAppearance: number;
 livesIn: string;
}
Listing 4-20

A Type Representing Output

Next, let’s create an event emitter and emit the object when the user clicks a button in the component. The event emitter is decorated with Output. In Listing 4-21, the event emitter and Output are imported from @angular/core.
import { Component, OnInit, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
export class SuperheroProfileComponent implements OnInit, OnChanges {
 @Input() name: string;
 @Input("lives-in") livesIn: string;
 @Output() flyWithSuperhero = new EventEmitter<Superhero>();
 // … rest of the component
}
Listing 4-21

Create an Event Emitter and Decorate It

The EventEmitter instance is of a generic type, Superhero. Hence, this event is strictly typed to emit the Superhero type. With the click of a button, the Superhero type object is emitted.
--- superhero-profile.component.html ---
 <button (click)="returnSuperheroData()"> Fly with a Superhero</button>
--- superhero-profile.component.ts
 returnSuperheroData(){
    let hero = {
      name: this.name,
      livesIn: this.livesIn,
      firstAppearance: this._firstAppearance
    };
    this.flyWithSuperhero.emit(hero);
  }
Listing 4-22

Emit the on click Event of the Button in the Component

Lifecycle Hooks

A component’s lifecycle is managed by the framework (Angular). It creates an instance of the component, updates data as required, and finally, destroys the component. The framework provides the ability to tap into various moments in the lifecycle and perform additional actions. These functions, which are called at various stages in the lifecycle, are known as hooks, or lifecycle hooks.

Consider the following list of lifecycle hooks. You may define one or more lifecycle hooks in a component. It is based on the need and the use case. Each lifecycle hook has a TypeScript interface that defines the contract and the signature for the lifecycle hook function. Implement the interface on the component class so that it enforces to define the function.
  • ngOnInit(): Invoked as the component is initialized. It is in addition to constructor. The constructor is meant for simple initializations, such as setting a default value on class variables. Use ngOnInit for more complex tasks, such as invoking a remote API to obtain data for the component. To define the ngOnInit() lifecycle hook, implement the OnInit interface, which is part of @angular/core (see Listing 4-22).

  • ngOnDestory(): Invoked as the component instance is destroyed. Perform cleanup actions in this function. To define the ngOnDestory() lifecycle hook, implement the OnDestroy interface, which is part of @angular/core (see Listing 4-22).

  • ngOnChanges(): Invoked as and when the values of the input attributes change. This provides control for the developer to perform additional actions if the input values meet certain criteria.

    Exercise caution when using this lifecycle hook. The number of times that this lifecycle hook is invoked is high. In a typical component, data-bound input attributes change often. If an additional action needs to be performed every time it happens, this could affect the application’s performance.

  • To define the ngOnChanges() lifecycle hook, implement the OnChanges interface, which is part of @angular/core.

  • ngDoCheck(): Not all changes to the input attributes are caught by the framework. Use ngDoCheck() for a manual check on all input attributes (or the variables of interest)on the component.

  • Exercise caution while using this lifecycle hook. This hook is invoked every time a change occurs in the application. Hence, using this lifecycle has a high cost to the application.

  • ngAfterViewInit() and ngAfterViewChecked(): The Angular application is a tree of parent/child components (or views). The ngAfterViewInit()lifecycle hook is called after the child view of the given component is initialized.

    The ngAfterViewChecked() lifecycle hook is called after data has been changed (updated) for the child views or components. This lifecycle hook is invoked in the parent component. Child components may use their own ngOnChanges() or ngDoCheck().

    To define the ngAfterViewInit() or ngAfterViewChecked() lifecycle hooks, implement the AfterViewInit and AfterViewChecked interfaces, respectively. The interfaces are part of @angular/core.

  • ngAfterContentInit() and ngAfterContentChecked(): These lifecycle hooks are in projected content, which is the markup provided between the component’s opening and closing tags. Refer to the “An example to showcase projecting content in Angular” section for more information on projected content. The ngAfterContentInit() lifecycle hook is called after the projected content is initialized. The ngAfterContentChecked() lifecycle hook is called after the projected content data has been changed (updated).

    To define the ngAfterContentInit() or ngAfterContentChecked() lifecycle hooks, implement the AfterContentInit and AfterContentChecked interfaces, respectively. The interfaces are part of @angular/core.

Listing 4-23 defines the ngOnInit() and ngOnDestroy() lifecycle hooks.
import { Component, OnInit, OnDestroy, } from '@angular/core';
@Component({
  selector: 'app-superhero',
  templateUrl: './superhero.component.html',
  styleUrls: ['./superhero.component.css']
})
export class SuperheroComponent implements OnInit, OnDestroy {
  constructor(private route: ActivatedRoute) {
        // Constructor for basic initialization of variables or assigning a default value.
  }
  ngOnInit() {
      console.log("ngOnInit Called");
      // Perform component initializations like calling an API here.
   }
   ngOnDestroy(){
     console.log("ngOnDestory called");
     // Perform component clean-up here.
   }
}
Listing 4-23

ngOnInit() and ngOnDestory

Note

Implementing an interface for a lifecycle hook is a good practice; however, it is not mandatory. We may define the lifecycle hook function without the interface. It will still be invoked by the framework. If you make a mistake with the signature or function name, the IDE and the compiler cannot highlight the error because we have not used an interface.

Conclusion

Angular components are fundamental to the library. In this chapter, we began by providing DOM tree context on a web page. HTML elements organized as a tree were introduced.

Next, I discussed the components and how to create them using Angular CLI. The ng generate command was used to create a component. It listed the files needed for building a component and their purposes.

Next, I elaborated on the various fields and configurations with the components. I explained the @Component TypeScript decorator. The decorator configures HTML templates and CSS style sheets as parameters. The TypeScript class encapsulates the presentation logic written in the TypeScript language.

Then we covered dependency injection and providing a TypeScript class or a service to the component. The component uses and depends on the TypeScript class or a service. A block of reusable code without a view can be encapsulated in the service.

The chapter concluded by discussing how components accept input, and use @Input decorator, provide output to the environment, and use @Output decorator with an event emitter.

References

Exercise

Create a component that shows the following information about four dinosaurs. Use a Material Design card component as a container of the component. Show a Like button that increments a counter every time a user clicks it.
  • Dinosaur 1

  • Herrerasaurus

  • A large predator

  • Herrerasauridae family

  • Dinosaur 2

  • Triceratops

  • An herbivorous dinosaur

  • Ceratopsidae family

  • Dinosaur 3

  • T. rex

  • Tyrannosaurus rex is abbreviated as T. rex.

  • Tyrannosauridae family

  • Dinosaur 4

  • Plateosaurus

  • The name means a broad lizard.

  • Plateosauridae family

  • Show a picture of the dinosaur.

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

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