Chapter 4. Understanding and Using Angular Components

In the previous chapter, we did a deep dive into the built-in directives that Angular offers that allow us to perform common functionality like hiding and showing elements, repeating templates, and so on. We worked with directives like ngIf and ngForOf and got a feel for how and when to use them.

In this chapter, we will go a bit deeper into components, those elements we have been creating to render the UI and let users interact with the applications we build. We will cover some of the more useful attributes you can specify when creating components, how to think about the lifecycle of the component and the various hooks that Angular gives you, and finally, cover how to pass data into and out of your custom components. By the end of the chapter, you should be able to perform most common tasks related to components while understanding what you are doing and why.

Components—A Recap

In the previous chapter, we saw that Angular only has directives, and that directives are reused for multiple purposes. We dealt with attribute and structural directives, which allow us to change the behavior of an existing element or to change the structure of the template being rendered.

The third kind of directives are components, which we have been using pretty much from the first chapter. To some extent, you can consider an Angular application to be nothing but a tree of components. Each component in turn has some behavior and a template that gets rendered. This template can then continue to use other components, thus forming a tree of components, which is the Angular application that gets rendered in the browser.

At its very simplest, a component is nothing but a class that encapsulates behavior (what data to load, what data to render, and how to respond to user interactions) and a template (how the data is rendered). But there are multiple ways to define that as well, along with other options, which we will cover in the following sections.

Defining a Component

We define a component using the TypeScript decorator Component. This allows us to annotate any class with some metadata that teaches Angular how the component works, what to render, and so on. Let’s take a look again at the stock-item component we created to see what a simple component would look like, and we will build up from there:

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}

The very basic component only needs a selector (to tell Angular how to find instances of the component being used) and a template (that Angular has to render when it finds the element). All other attributes in the Component decorator are optional. In the preceding example, we have defined that the StockItemComponent is to be rendered whenever Angular encounters the app-stock-item selector, and to render the stock-item.component.html file when it encounters the element. Let’s talk about the attributes of the decorator in a bit more detail.

Selector

The selector attribute, as we touched upon briefly in Chapter 2, allows us to define how Angular identifies when the component is used in HTML. The selector takes a string value, which is the CSS selector Angular will use to identify the element. The recommended practice when we create new components is to use element selectors (like we did with app-stock-item), but technically you could use any other selector as well. For example, here are a few ways you could specify the selector attribute and how you would use it in the HTML:

  • selector: 'app-stock-item' would result in the component being used as <app-stock-item></app-stock-item> in the HTML.

  • selector: '.app-stock-item' would result in the component being used as a CSS class like <div class="app-stock-item"></div> in the HTML.

  • selector: '[app-stock-item]' would result in the component being used as an attribute on an existing element like <div app-stock-item></div> in the HTML.

You can make the selector as simple or complex as you want, but as a rule of thumb, try to stick to simple element selectors unless you have a very strong reason not to.

Template

We have been using templateUrl so far to define what the template to be used along with a component is. The path you pass to the templateUrl attribute is relative to the path of the component. In the previous case, we can either specify the templateUrl as:

templateUrl: './stock.item.component.html'

or:

templateUrl: 'stock.item.component.html'

and it would work. But if you try to specify an absolute URL or anything else, your compilation would break. One interesting thing to note is that unlike AngularJS (1.x), the application Angular builds does not load the template by URL at runtime. Instead, Angular precompiles a build and ensures that the template is inlined as part of the build process.

Instead of templateUrl, we could also specify the template inline in the component, using the template option. This allows us to have the component contain all the information instead of splitting it across HTML and TypeScript code.

Tip

Only one of template and templateUrl can be specified in a component. You cannot use both, but at least one is essential.

There is no impact on the final generated application as Angular compiles the code into a single bundle. The only reason you might want to split your template code into a separate file is to get nicer IDE features such as syntax completion and the like, which are specific to file extensions. Generally, you might want to keep your templates separate if they are over three or four lines or have any complexity.

Let’s see how our stock-item component might look with an inline template:

import { Component, OnInit } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  template: `
	<div class="stock-container">
	  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
	  <div class="price"
	      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
	      $ {{stock.price}}
   </div>
	  <button (click)="toggleFavorite($event)"
	          *ngIf="!stock.favorite">Add to Favorite</button>
	</div>
  `,
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}
Tip

ECMAScript 2015 (and TypeScript) allows us to define multiline templates using the ` (backtick) symbol, instead of doing string concatenation across multiple lines using the + (plus) operator. We leverage this usually when we define inline templates.

You can find the completed code in the chapter4/component-template folder in the GitHub repository.

All we have done is taken the template and moved it into the template attribute of the Component decorator. In this particular case though, because there are more than a few lines with some amount of work being done, I would recommend not moving it inline. Note that as a result of moving it to template, we have removed the previous templateUrl attribute.

Styles

A given component can have multiple styles attached to it. This allows you to pull in component-specific CSS, as well as potentially any other common CSS that needs to be applied to it. Similar to templates, you can either inline your CSS using the styles attribute, or if there is a significant amount of CSS or you want to leverage your IDE, you can pull it out into a separate file and pull it into your component using the styleUrls attribute. Both of these take an array as an input.

One thing that Angular promotes out of the box is complete encapsulation and isolation of styles. That means by default, the styles you define and use in one component will not affect/impact any other parent or child component. This ensures that you can be confident that the CSS classes you define in any component will not unknowingly affect anything else, unless you explicitly pull in the necessary styles.

Again, just like templates, Angular will not pull in these styles at runtime, but rather precompile and create a bundle with the necessary styles. Thus, the choice of using styles or styleUrls is a personal one, without any major impact at runtime.

Warning

Do not use both styles and styleUrls together. Angular will end up picking one or the other and will lead to unexpected behavior.

Let’s quickly see how the component might look if we inlined the styles:

import { Component, OnInit } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: 'stock-item.component.html',
  styles: [`
    .stock-container {
      border: 1px solid black;
      border-radius: 5px;
      display: inline-block;
      padding: 10px;
    }

    .positive {
      color: green;
    }

    .negative {
      color: red;
    }
  `]
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}

You can find the completed code in the chapter4/component-style folder in the GitHub repository.

You can of course choose to pass in multiple style strings to the attribute. The decision between using styles and styleUrls is one of personal preference and has no impact on the final performance of the application.

Style Encapsulation

In the preceding section, we talked about how Angular encapsulates the styles to ensure that it doesn’t contaminate any of your other components. In fact, you can actually tell Angular whether it needs to do this or not, or if the styles can be accessible globally. You can set this by using the encapsulation attribute on the Component decorator. The encapsulation attribute takes one of three values:

ViewEncapsulation.Emulated

This the default, where Angular creates shimmed CSS to emulate the behavior that shadow DOMs and shadow roots provide.

ViewEncapsulation.Native

This is the ideal, where Angular will use shadow roots. This will only work on browsers and platforms that natively support it.

ViewEncapsulation.None

Uses global CSS, without any encapsulation.

What Is the Shadow DOM?

HTML, CSS, and JavaScript have a default tendency to be global in the context of the current page. What this means is that an ID given to an element can easily clash with another element somewhere else on the page. Similarly, a CSS rule given to a button in one corner of the page might end up impacting another totally unrelated button.

We end up having to come up with specific naming conventions, use CSS hacks like !important, and use many more techniques to work around this generally in our day-to-day development.

Shadow DOM fixes this by scoping HTML DOM and CSS. It provides the ability to have scoped styling to a component (thus preventing the styles from leaking out and affecting the rest of the application) and also the ability to isolate and make the DOM self-contained.

You can read up on it more in the documentation for self-contained web components.

The best way to see how this impacts our application is to make a slight change and see how our application behaves under different circumstances.

First, let’s add the following snippet of code to the app.component.css file. We are using the same base as the previous chapter, and the completed code is available in the chapter4/component-style-encapsulation folder:

.name {
  font-size: 50px;
}

If we run the application right now, there is no impact on our application. Now, let’s try changing the encapsulation property on the main AppComponent. We will change the component as follows:

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class AppComponent {
  title = 'app works!';
}

We added the encapsulation: ViewEncapsulation.None line to our Component decorator (of course, after importing the ViewEncapsulation enum from Angular). Now if we refresh our application, you will see that the name of the stock has been blown up to 50px. This is because the styles applied on the AppComponent are not restricted to just the component but are now taking the global namespace. Thus, any element that adds the name class to itself will get this font-size applied to it.

ViewEncapsulation.None is a good way of applying common styles to all child components, but definitely adds the risk of polluting the global CSS namespace and having unintentional effects.

Others

There are a lot more attributes than what we covered on the Component decorator. We will briefly review a few of those here, and will reserve discussion of others for later chapters when they become more relevant. Here is a quick highlight of some of the other major attributes and their uses:

Stripping white spaces

Angular allows you to strip any unnecessary white spaces from your template (as defined by Angular, including more than one space, space between elements, etc.). This can help reduce the build size by compressing your HTML. You can set this feature (which is set to false by default) by using the preserveWhitespaces attribute on the component. You can read more about this feature in the official documentation.

Animations

Angular gives you multiple triggers to control and animate each part of the component and its lifecycle. To accomplish this, it provides its own DSL, which allows Angular to animate on state changes within the element.

Interpolation

There are times when the default Angular interpolation markers (the double-curlies {{ and }}) interfere with integrating with other frameworks or technologies. For those scenarios, Angular allows you to override the interpolation identifiers at a component level by specifying the start and end delimiters. You can do so by using the interpolation attribute, which takes an array of two strings, the opening and closing markers for the interpolation. By default, they are ['{{', '}}'], but you override it by, say, providing interpolation: ['<<', '>>'] to replace the interpolation symbols for just that component to << and >>.

View providers

View providers allow you to define providers that inject classes/services into a component or any of its children. Usually, you won’t need it, but if there are certain components where you want to override, or restrict the availability of a class or a service, you can specify an array of providers to a component using the viewProviders attribute. We will cover this in more detail in Chapter 8.

Exporting the component

We have been working so far by using the component class’s functions within the context of the template. But there are use cases (especially when we start dealing with directives and more complex components) for which we might want to allow the user of the component to call functions on the component from outside. A use case might be that we provide a carousel component, but want to provide functionality to allow the user of the component to control the next/previous functionality. In these cases, we can use the exportAs attribute of the Component decorator.

changeDetection

By default, Angular checks every binding in the UI to see if it needs to update any UI element whenever any value changes in our component. This is acceptable for most applications, but as our applications get larger in size and complexity, we might want control over how and when Angular updates the UI. Instead of Angular deciding when it needs to update the UI, we might want to be explicit and tell Angular when it needs to update the UI manually. To do this, we use the changeDetection attribute, where we can override the default value of Change​DetectionStrategy.Default to ChangeDetectionStrategy.OnPush. This means that after the initial render, it will be up to us to let Angular know when the value changes. Angular will not check the component’s bindings automatically. We will cover this in more detail later in the chapter.

There are a lot more attributes and features with regards to components that we don’t cover in this chapter. You should take a look at the official documentation for components to get familiar with what else is possible, or dive deeper into the details.

Components and Modules

Before we go into the details of the lifecycle of a component, let’s quickly sidetrack into how components are linked to modules and what their relation is. In Chapter 2, we saw how any time we created a new component, we had to include it in a module. If you create a new component, and do not add it to a module, Angular will complain that you have components that are not part of any modules.

For any component to be used within the context of a module, it has to be imported into your module declaration file and declared in the declarations array. This ensures that the component is visible to all other components within the module.

There are three specific attributes on the NgModule that directly impact components and their usage, which are important to know. While only declarations is important initially, once you start working with multiple modules, or if you are either creating or importing other modules, the other two attributes become essential:

declarations

The declarations attribute ensures that components and directives are available to use within the scope of the module. The Angular CLI will automatically add your component or directive to the module when you create a component through it. When you first start out building Angular applications, you might easily forget to add your newly created components to the declarations attribute, so keep track of that (if you are not using the Angular CLI, that is!) in order to avoid this common mistake.

imports

The imports attribute allows you to specify modules that you want imported and accessible within your module. This is mostly as a way to pull in third-party modules to make the components and services available within your application. If you want to use a component from other modules, make sure you import the relevant modules into the module you have declared and where the component exists.

exports

The exports attribute is relevant if you either have multiple modules or you need to create a library that will be used by other developers. Unless you export a component, it cannot be accessed or used outside of the direct module where the component is declared. As a general rule of thumb, if you will need to use the component in another module, make sure you export it.

Tip

If you are facing issues using a component, where Angular fails to recognize a component or says it does not recognize an element, it most likely is due to misconfigured modules. Check, in order, the following:

  • Whether the component is added as a declaration in the module.

  • In case it is not a component that you wrote, make sure that you have imported the module that provides/exports the component.

  • If you created a new component that needs to be used in other components, make sure that you export the component in its module so that any application including the module will get access to your newly created component.

Input and Output

One common use case when we start creating components is that we want to separate the content that a component uses from the component itself. A component is truly useful when it is reusable. One of the ways we can make a component reusable (rather than having default, hardcoded values inside it) is by passing in different inputs depending on the use case. Similarly, there might be cases where we want hooks from a component when a certain activity happens within its context.

Angular provides hooks to specify each of these through decorators, aptly named Input and Output. These, unlike the Component and NgModule decorators, apply at a class member variable level.

Input

When we add an Input decorator on a member variable, it automatically allows you to pass in values to the component for that particular input via Angular’s data binding syntax.

Let’s see how we can extend our stock-item component from the previous chapter to allow us to pass in the stock object, rather than hardcoding it within the component itself. The finished example is available in the GitHub repository in the chapter4/component-input folder. If you want to code along and don’t have the previous code, you can use the chapter3/ng-if codebase as the starter to code along from.

We will first modify the stock-item component to mark the stock as an input to the component, but instead of initializing the stock object, we will mark it as an Input to the component. We do this by importing the decorator and using it for the stock variable. The code for the stock-item.component.ts file should look like the following:

import { Component, OnInit, Input } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent {

  @Input() public stock: Stock;

  constructor() { }

  toggleFavorite(event) {
    this.stock.favorite = !this.stock.favorite;
  }
}

We have removed all instantiation logic from the app-stock-item component, and marked the stock variable as an input. This means that the initialization logic has been moved out, and the component is only responsible for receiving the value of the stock from the parent component and just rendering the data.

Next, let’s take a look at the AppComponent and how we can change that to now pass in the data to the StockItemComponent:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'Stock Market App';

  public stockObj: Stock;

  ngOnInit(): void {
    this.stockObj = new Stock('Test Stock Company', 'TSC', 85, 80);
  }
}

We just moved the initialization of the stock object from the StockItemComponent to the AppComponent. Finally, let’s take a look at the template of the AppComponent to see how we can pass in the stock to the StockItemComponent:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stockObj"></app-stock-item>

We use Angular’s data binding to pass in the stock from the AppComponent to the StockItemComponent. The name of the attribute (stock) has to match the name of the variable in the component that has been marked as input. The attribute name is case sensitive, so make sure it matches exactly with the input variable name. The value that we pass to it is the reference of the object in the AppComponent class, which is stockObj.

HTML and Case-Sensitive Attributes?

You might wonder how this is even possible. Angular has its own HTML parser under the covers that parses the templates for Angular-specific syntax, and does not rely on the DOM API for some of these. This is why Angular attributes are and can be case-sensitive.

These inputs are data bound, so if you end up changing the value of the object in AppComponent, it will automatically be reflected in the child StockItemComponent.

Output

Just like we can pass data into a component, we can also register and listen for events from a component. We use data binding to pass data in, and we use event binding syntax to register for events. We use the Output decorator to accomplish this.

We register an EventEmitter as an output from any component. We can then trigger the event using the EventEmitter object, which will allow any component bound to the event to get the notification and act accordingly.

We can use the code from the previous example where we registered an Input decorator and continue on from there. Let’s now extend the StockComponent to trigger an event when it is favorited, and move the data manipulation out from the component to its parent. This makes sense as well because the parent component is responsible for the data and should be the single source of truth. Thus, we will let the parent AppComponent register for the toggleFavorite event and change the state of the stock when the event is triggered.

The finished code for this is in the chapter4/component-output folder.

Take a look at the StockItemComponent code in src/app/stock/stock-item/stock-item.component.ts:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }
}

A few important things to note:

  • We imported the Output decorator as well as the EventEmitter from the Angular library.

  • We created a new class member called toggleFavorite of type EventEmitter, and renamed our method to onToggleFavorite. The EventEmitter can be typed for additional type safety.

  • We need to ensure that the EventEmitter instance is initialized, as it is not auto-initialized for us. Either do it inline or do it in the constructor as we did earlier.

  • The onToggleFavorite now just calls a method on the EventEmitter to emit the entire stock object. This means that all listeners of the toggleFavorite event will get the current stock object as a parameter.

We will also change stock-item.component.html to call the onToggleFavorite method instead of toggleFavorite. The HTML markup remains pretty much the same otherwise:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <button (click)="onToggleFavorite($event)"
          *ngIf="!stock.favorite">Add to Favorite</button>
</div>

Next, we add a method to the AppComponent that should be triggered whenever the onToggleFavorite method is triggered, which we will add event binding on:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';

  public stock: Stock;

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company', 'TSC', 85, 80);
  }

  onToggleFavorite(stock: Stock) {
    console.log('Favorite for stock ', stock, ' was triggered');
    this.stock.favorite = !this.stock.favorite;
  }
}

The only thing new is the onToggleFavorite method we have added, which takes a stock as an argument. In this particular case, we don’t use the stock passed to it other than for logging, but you could base any decision/work on that. Note also that the name of the function is not relevant, and you could name it whatever you want.

Finally, let’s tie it all together by subscribing to the new output from our StockComponent in the app-component.html file:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stock"
                (toggleFavorite)="onToggleFavorite($event)">
</app-stock-item>

We just added an event binding using Angular’s event-binding syntax to the output declared in the stock-item component. Notice again that it is case sensitive and it has to exactly match what member variable we decorated with the Output decorator. Also, to get access to the value emitted by the component, we use the keyword $event as a parameter to the function. Without it, the function would still get triggered, but you would not get any arguments with it.

With this, if you run the application (remember, ng serve), you should see the fully functional app, and when you click the Add to Favorite button, it should trigger the method in the AppComponent.

Change Detection

We mentioned changeDetection as an attribute on the Component decorator. Now that we have seen how Input and Output decorators work, let’s deep dive a little bit into how Angular performs its change detection at a component level.

By default, Angular applies the ChangeDetectionStrategy.Default mechanism to the changeDetection attribute. This means that every time Angular notices an event (say, a server response or a user interaction), it will go through each component in the component tree, and check each of the bindings individually to see if any of the values have changed and need to be updated in the view.

For a very large application, you will have lots of bindings on a given page. When a user takes any action, you as a developer might know for sure that most of the page will not change. In such cases, you can actually give a hint to the Angular change detector to check or not check certain components as you see fit. For any given component, we can accomplish this by changing the ChangeDetectionStrategy from the default to ChangeDetectionStrategy.OnPush. What this tells Angular is that the bindings for this particular component will need to be checked only based on the Input to this component.

Let’s consider a few examples to see how this might play out. Say we have a component tree A → B → C. That is, we have a root component A, which uses a component B in its template, which in turn uses a component C. And let’s say component B passes in a composite object compositeObj to component C as input. Maybe something like:

<c [inputToC]="compositeObj"></c>

That is, inputToC is the input variable marked with the Input decorator in component C, and is passed the object compositeObj from component B. Now say we marked component C’s changeDetection attribute as ChangeDetectionStrategy​.OnPush. Here are the implications of that change:

  • If component C has bindings to any attributes of compositeObj, they will work as usual (no change from default behavior).

  • If component C makes any changes to any of the attributes of compositeObj, they will also be updated immediately (no change from default behavior).

  • If the parent component B creates a new compositeObj or changes the reference of compositeObj (think new operator, or assign from a server response), then component C would recognize the change and update its bindings for the new value (no change from default behavior, but internal behavior changes on how Angular recognizes the change).

  • If the parent component B changes any attribute on the compositeObj directly (as a response to a user action outside component B), then these changes would not be updated in component C (major change from the default behavior).

  • If the parent component B changes any attribute on response to an event emitter from component C, and then changes any attribute on the compositeObj (without changing the reference), this would still work and the bindings would get updated. This is because the change originates from component C (no change from default behavior).

Angular provides ways for us to signal when to check the bindings from within the component as well, to have absolute control on Angular’s data binding. We will cover these in “Change Detection”. For now, it is good to understand the difference between the two change detection strategies that Angular provides.

Let’s now modify the example code to see this in action. First, modify the stock-item.component.ts file to change the ChangeDetectionStrategy in the child component:

import { Component, OnInit, Input, Output } from '@angular/core';
import { EventEmitter, ChangeDetectionStrategy } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StockItemComponent {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }

  changeStockPrice() {
    this.stock.price += 5;
  }
}

In addition to changing the ChangeDetectionStrategy, we also added another function to changeStockPrice(). We will use these functions to demonstrate the behavior of the change detection in the context of our application.

Next, let’s quickly modify stock-item.component.html to allow us to trigger the new function. We will simply add a new button to trigger and change the stock price when the button is clicked:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <button (click)="onToggleFavorite($event)"
          *ngIf="!stock.favorite">Add to Favorite</button>
  <button (click)="changeStockPrice()">Change Price</button>
</div>

There is no change to the HTML of the template other than adding a new button to change the stock price. Similarly, let’s quickly change the main app.component.html file to add another button to trigger the change of the price from the parent component (similar to component B in the earlier hypothetical example):

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stock"
                (toggleFavorite)="onToggleFavorite($event)">
</app-stock-item>
<button (click)="changeStockObject()">Change Stock</button>
<button (click)="changeStockPrice()">Change Price</button>

We have added two new buttons to this template: one that will change the reference of the stock object directly, and another that will modify the existing reference of the stock object to change the price from the parent AppComponent. Now finally, we can see how all of this is hooked up in the app.component.ts file:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';

  public stock: Stock;
  private counter: number = 1;

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company - ' + this.counter++,
        'TSC', 85, 80);
  }

  onToggleFavorite(stock: Stock) {
    // This will update the value in the stock item component
    // Because it is triggered as a result of an event
    // binding from the stock item component
    this.stock.favorite = !this.stock.favorite;
  }

  changeStockObject() {
    // This will update the value in the stock item component
    // Because we are creating a new reference for the stock input
    this.stock = new Stock('Test Stock Company - ' + this.counter++,
        'TSC', 85, 80);
  }

  changeStockPrice() {
    // This will not update the value in the stock item component
    // because it is changing the same reference and angular will
    // not check for it in the OnPush change detection strategy.
    this.stock.price += 10;
  }
}

The app.component.ts file has seen the most changes. The preceding code is also well annotated with comments to explain the expected behavior when each of these functions are triggered. We have added two new methods: changeStockObject(), which creates a new instance of the stock object in the AppComponent, and changeStockPrice(), which modifies the prices of the stock object in the AppComponent. We have also added a counter just to keep track of how many times we create a new stock object, but that is not strictly necessary.

Now when you run this application, you should expect to see the following behavior:

  • Clicking Add to Favorite within the StockItemComponent still works as expected.

  • Clicking Change Price within the StockItemComponent will increase the price of the stock by $5 each time.

  • Clicking Change Stock outside the StockItemComponent will change the name of the stock with each click. (This is why we added the counter!)

  • Clicking Change Price outside the StockItemComponent will have no impact (even though the actual value of the stock will jump if you click Change Price inside after this). This shows that the model is getting updated, but Angular is not updating the view.

You should also change back the ChangeDetectionStrategy to default to see the difference in action.

Component Lifecycle

Components (and directives) in Angular have their own lifecycle, from creation, rendering, changing, to destruction. This lifecycle executes in preorder tree traversal order, from top to bottom. After Angular renders a component, it starts the lifecycle for each of its children, and so on until the entire application is rendered.

There are times when these lifecycle events are useful to us in developing our application, so Angular provides hooks into this lifecycle so that we can observe and react as necessary. Figure 4-1 shows the lifecycle hooks of a component, in the order in which they are invoked.

Angular Component Lifecycle
Figure 4-1. Angular component lifecycle hooks (original from https://angular.io/guide/lifecycle-hooks)

Angular will first call the constructor for any component, and then the various steps mentioned earlier in order. Some of them, like the OnInit and AfterContentInit (basically, any lifecycle hook ending with Init) is called only once, when a component is initialized, while the others are called whenever any content changes. The OnDestroy hook is also called only once for a component.

Each of these lifecycle steps comes with an interface that should be implemented when a component cares about that particular lifecycle, and each interface provides a function starting with ng that needs to be implemented. For example, the OnInit lifecycle step needs a function called ngOnInit to be implemented in the component and so on.

We will walk through each of the lifecycle steps here, and then use one example to see this all in action and the ordering of lifecycle steps within a component and across components.

There is also one more concept to learn, which we will briefly touch upon in this chapter, and come back to later in more detail—the concept of ViewChildren and ContentChildren.

ViewChildren is any child component whose tags/selectors (mostly elements, as that is the recommendation for components) appear within the template of the component. So in our case, app-stock-item would be a ViewChild of the AppComponent.

ContentChildren is any child component that gets projected into the view of the component, but is not directly included in the template within the component. Imagine something like a carousel, where the functionality is encapsulated in the component, but the view, which could be images or pages of a book, comes from the user of the component. Those are generally achieved through ContentChildren. We will cover this in more depth later in this chapter.

Interfaces and Functions

Table 4-1 shows the interfaces and functions in the order in which they are called, along with specific details about the step if there is anything to note. Note that we are only covering component-specific lifecycle steps, and they are slightly different from a directive’s lifecycle.

Table 4-1. Angular lifecycle hooks and methods
Interface Method Applicable to Purpose

OnChanges

ngOnChanges(changes: SimpleChange)

Components and directives

ngOnChanges is called both right after the constructor to set and then later every time the input properties to a directive change. It is called before the ngOnInit method.

OnInit

ngOnInit()

Components and directives

This is your typical initialization hook, allowing you to do any one-time initialization specific to your component or directive. This is the ideal place to load data from the server and so on, rather than the constructor, both for separation of concerns as well as testability.

DoCheck

ngDoCheck()

Components and directives

DoCheck is Angular’s way of giving the component a way to check if there are any bindings or changes that Angular can’t or should not detect on its own. This is one of the ways we can use to notify Angular of a change in the component, when we override the default ChangeDetectionStrategy for a component from Default to OnPush.

After​Con⁠tent​Init

ngAfterContent​Init()

Components only

As mentioned, the AfterContentInit hook is triggered during component projection cases, and only once during initialization of the component. If there is no projection, this is triggered immediately.

After​Con⁠tent​Checked

ngAfterContent​Checked()

Components only

AfterContentChecked is triggered each time Angular’s change detection cycle executes, and in case it is initialization, it is triggered right after the AfterContentInit hook.

AfterView​Init

ngAfterView​Init()

Components only

AfterViewInit is the complement to AfterContent​Init, and is triggered after all the child components that are directly used in the template of the component are finished initializing and their views updated with bindings. This may not necessarily mean that the views are rendered into the browser, but that Angular has finished updating its internal views to render as soon as possible. AfterViewInit is triggered only once during the load of the component.

AfterViewChecked

ngAfterViewChecked()

Components only

AfterViewChecked is triggered each time after all the child components have been checked and updated. Again, a good way to think about both this and AfterContent​Checked is like a depth-first tree traversal, in that it will execute only after all the children components’ AfterViewChecked hooks have finished executing.

OnDestroy

ngOnDestroy()

Components and directives

The OnDestroy hook is called when a component is about to be destroyed and removed from the UI. It is a good place to do all cleanup, like unsubscribing any listeners you may have initialized and the like. It is generally good practice to clean up anything that you have registered (timers, observables, etc.) as part of the component.

Let’s try to add all these hooks to our existing application to see the order of execution in a real-world scenario. We will add all of these hooks to both our AppComponent and the StockItemComponent, with a simple console.log to just see when and how these functions are executed. We will use the base from the output example to build from, so in case you are not coding along, you can take the example from chapter4/component-output to build from there.

The final finished example is also available in chapter4/component-lifecycle.

First, we can modify the src/app/app.component.ts file and add the hooks as follows:

import { Component, SimpleChanges, OnInit, OnChanges, OnDestroy,
         DoCheck, AfterViewChecked, AfterViewInit, AfterContentChecked,
         AfterContentInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnChanges, OnDestroy,
                                     DoCheck, AfterContentChecked,
                                     AfterContentInit, AfterViewChecked,
                                     AfterViewInit {
  title = 'app works!';

  public stock: Stock;

  onToggleFavorite(stock: Stock) {
    console.log('Favorite for stock ', stock, ' was triggered');
    this.stock.favorite = !this.stock.favorite;
  }

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company', 'TSC', 85, 80);
    console.log('App Component - On Init');
  }

  ngAfterViewInit(): void {
    console.log('App Component - After View Init');
  }
  ngAfterViewChecked(): void {
    console.log('App Component - After View Checked');
  }
  ngAfterContentInit(): void {
    console.log('App Component - After Content Init');
  }
  ngAfterContentChecked(): void {
    console.log('App Component - After Content Checked');
  }
  ngDoCheck(): void {
    console.log('App Component - Do Check');
  }
  ngOnDestroy(): void {
    console.log('App Component - On Destroy');
  }
  ngOnChanges(changes: SimpleChanges): void {
    console.log('App Component - On Changes - ', changes);
  }
}

You can see that we have implemented the interfaces for OnInit, OnChanges, OnDestroy, DoCheck, AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit on the AppComponent class, and then went ahead and implemented the respective functions. Each of the methods simply prints out a log statement mentioning the component name and the trigger method name.

Similarly, we can do the same for the StockItemComponent:

import { Component, SimpleChanges, OnInit,
         OnChanges, OnDestroy, DoCheck, AfterViewChecked,
         AfterViewInit, AfterContentChecked,
         AfterContentInit, Input,
         Output, EventEmitter } from '@angular/core';
import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit, OnChanges,
                                           OnDestroy, DoCheck,
                                           AfterContentChecked,
                                           AfterContentInit,
                                           AfterViewChecked,
                                           AfterViewInit {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }

  ngOnInit(): void {
    console.log('Stock Item Component - On Init');
  }
  ngAfterViewInit(): void {
    console.log('Stock Item Component - After View Init');
  }
  ngAfterViewChecked(): void {
    console.log('Stock Item Component - After View Checked');
  }
  ngAfterContentInit(): void {
    console.log('Stock Item Component - After Content Init');
  }
  ngAfterContentChecked(): void {
    console.log('Stock Item Component - After Content Checked');
  }
  ngDoCheck(): void {
    console.log('Stock Item Component - Do Check');
  }
  ngOnDestroy(): void {
    console.log('Stock Item Component - On Destroy');
  }
  ngOnChanges(changes: SimpleChanges): void {
    console.log('Stock Item Component - On Changes - ', changes);
  }
}

We have done exactly the same thing we did on the AppComponent with the Stock​ItemComponent. Now, we can run this application to see it in action.

When you run it, open the JavaScript console in the browser. You should see, in order of execution:

  1. First, the AppComponent gets created. Then the following hooks are triggered on the AppComponent:

    • On Init

    • Do Check

    • After Content Init

    • After Content Checked

    The preceding two immediately execute because we don’t have any content projection in our application so far.

  2. Next, the StockItemComponent OnChanges executes, with the input to the Stock​ItemComponent being recognized as the change, followed by the hooks listed here within the StockItemComponent:

    • On Init

    • Do Check

    • After Content Init

    • After Content Checked

    • After View Init

    • After View Checked

  3. Finally, there are no more subcomponents to traverse down on, so Angular steps back out to the parent AppComponent, and executes the following:

    • After View Init

    • After View Checked

This gives us a nice view of how and in which order Angular goes around initializing and the tree traversal it does under the covers. These hooks become very useful for certain trickier initialization logic, and are definitely essential for cleanup once your component is done and dusted, to avoid memory leaks.

View Projection

The last thing we will cover in this chapter is the concept of view projection. Projection is an important idea in Angular as it gives us more flexibility when we develop our components and again gives us another tool to make them truly reusable under different contexts.

Projection is useful when we want to build components but set some parts of the UI of the component to not be an innate part of it. For example, say we were building a component for a carousel. A carousel has a few simple capabilities: it is able to display an item, and allow us to navigate to the next/previous element. Your carousel component might also have other features like lazy loading, etc. But one thing that is not the purview of the carousel component is the content it displays. A user of the component might want to display an image, a page of a book, or any other random thing.

Thus, in these cases, the view would be controlled by the user of the component, and the functionality would be provided by the component itself. This is but one use case where we might want to use projection in our components.

Let’s see how we might use content projection in our Angular application. We will use the base from the input example to build from, so in case you are not coding along, you can take the example from chapter4/component-input to build from there.

The final finished example is available in chapter4/component-projection.

First, we will modify our StockItemComponent to allow for content projection. There is no code change in our component class; we only need to modify the src/app/stock/stock-item/stock-item.component.html file as follows:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <ng-content></ng-content>             1
</div>
1

The new ng-content element for projection

We have simply removed the buttons we previously had, and are going to let the user of the component decide what buttons are to be shown. To allow for this, we have replaced the buttons with an ng-content element. There is no other change required in the component.

Next, we will make a change to the AppComponent, simply to add a method for testing purposes. Modify the src/app/app.component.ts file as follows:

/** Imports and decorators skipped for brevity **/

export class AppComponent implements OnInit {
  /** Constructor and OnInit skipped for brevity **/

  testMethod() {
    console.log('Test method in AppComponent triggered');
  }
}

We have simply added a method that will log to the console when it is triggered. With this in place, now let’s see how we can use our updated StockItemComponent and use the power of projection. Modify the app.component.html file as follows:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stockObj">
  <button (click)="testMethod()">With Button 1</button>
</app-stock-item>

<app-stock-item [stock]="stockObj">
  No buttons for you!!
</app-stock-item>

We have added two instances of the app-stock-item component in our HTML. And both of these now have some content inside them, as opposed to previously where these elements had no content. In one, we have a button that triggers the testMethod we added in the AppComponent, and the other simply has text content.

When we run our Angular application and open it in the browser, we should see something like Figure 4-2.

Notice that the two stock item components on our browser, each with slightly different content, are based on what we provided. If you click the button in the first stock widget, you will see that the method in the AppComponent gets called and the console.log is triggered.

Thus, users of the component now have the capability to change part of the UI of the component as they see fit. We can even access functionality from the parent component as well, which makes it truly flexible. It is also possible to project multiple different sections and content into our child component. While the official Angular documentation is spare on this topic, there is a great article that can give you more insight on content projection.

Angular Content Projection
Figure 4-2. Angular app with view projection

Conclusion

In this chapter, we went into a lot more depth on components, and saw some of the more commonly used attributes when creating components. We took a detailed look at the Component decorator, talking about attributes like template versus template​Url, styles, and also covered at a high level how Angular’s change detection works and how we can override it.

We then covered the lifecycle of a component, as well as the hooks that Angular provides for us to hook on to and react to some of these lifecycle events. Finally, we covered projection in components and how we can make some truly powerful components that allow the user of the component to decide parts of the UI.

In the next chapter, we will do a quick detour to understand unit testing of components, and see how we can test both the logic that drives the component as well as the view that gets rendered.

Exercise

For our third exercise, we can build on top of the previous exercise (chapter3/exercise) by including concepts from this chapter:

  1. Create a ProductListComponent. Initialize an array of products there, instead of initializing a single product in the ProductComponent. Change its template to use NgFor to create a ProductItemComponent for each product.

  2. Use inline templates and styles on the ProductListComponent. Generate it using the Angular CLI with that setting rather than generating it and changing it manually.

  3. Change the ProductItemComponent to take the product as an input.

  4. Move the increment/decrement logic from the ProductItem to the ProductListComponent. Use an index or product ID to find the product and change its quantity.

  5. Move the ProductItemComponent to be optimal and move from the default ChangeDetectionStrategy to an OnPush ChangeDetectionStrategy.

All of this can be accomplished using concepts covered in this chapter. You can check out the finished solution in chapter4/exercise/ecommerce.

Get Angular: Up and Running now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.