Nested Component

So far we have built applications with a two component, one is AppComponent and other one is ProductsComponent. our goal now is to start building applications that are composed of multiple components working together.

Breaking up an application into multiple logical components makes it easier to:

  • Architect an application as it grows in complexity.
  • Re-use common components in multiple places.

So in our application, instead of showing product list in the tabular form we will display each product with a different component called ProductComponent. ProductComponent will be the child component of ProductsComponent, that is also known as the Nested Component.

Don’t confuse here, ProductComponent represents individual product object, while ProductsComponent represents the list of products. 

So ProductsComponent will be the parent component of ProductComponent.

Excecute below command on terminal.

ng g c product

As shown below, this command will generate one product folder and in that folder, four files are created, also app.module.ts will be updated.

Product Component
ng g c product

Create one product property as below in product.component.ts

product: Product; // product property of Type Product.

We will use product property to display product information on a screen.

Replace product.component.html with below code,

<div class="card bg-secondary text-white">
  <div class="card-body">
    <h4 class="card-title">{{product.title}}</h4>
    <h5 class="card-text">{{product.price|currency:'INR'}}</h5>
  </div>
  <table class="table">
    <tr>
      <td scope="row">
        <b>Product Type</b>
      </td>
      <td>
        <i class="fa fa-laptop fa-2x" aria-hidden="true" *ngIf="product.productType=='Laptop'"></i>
        <i class="fa fa-mobile fa-2x" *ngIf="product.productType=='Mobile'" aria-hidden="true"></i>
      </td>
    </tr>
    <tr>
      <td scope="row">
        <b>Brand</b>
      </td>
      <td>{{product.brand}}</td>
    </tr>
  </table>
</div>

In above template, We have displayed the Product Title, Product Price and Brand using interpolation as {{product.productTitle}},{{product.price|currancy:'INR'}} and {{product.brand}} respectively.

<i class="fa fa-laptop fa-2x" aria-hidden="true"></i>

This code will display an icon for a laptop. Same way use other font awesome icons.

I have used here,  two font awesome classes ‘fa fa-laptop fa-2x‘ and ‘fa fa-mobile fa-2x‘ to display laptop and mobile icon.

If you have installed Bootstrap and Font Awesome Snippets Extension then, just write fa-<icon-name> on an html page, and it will generate the icon code for you.

For example to generate address-book icon, just write fa-address-book.

In above code, we have used *ngIf.  

<i class="fa fa-laptop fa-2x" aria-hidden="true" *ngIf="product.productType=='Laptop'"></i>

*ngIf is the structural directive. which is used to change the structure of DOM based on condition.

If the condition is true, then HTML tag will load in DOM.

You can use *ngIf with any HTML tag as well as with Angular Component selectors. like <div>, <input>, <i>, <products> etc. 

a product.component.html template will display product information as below.

Product Component View

But note that we will not able to see above component because to display any component on screen, we have to use a selector of that component in parent component.

So to display ProductComponent as a part of ProductsComponent, we need to add <app-product> selector of ProductComponent in products.component.html.

Replace products.component.html with below code,

<div class="card" >
  <div class="card-body">
    <h4 class="card-title">Products</h4>
    <div class="row">
      <app-product style="margin-bottom: 10px" class="col-md-3" *ngFor="let p of products"></app-product>
    </div>  
  </div>
</div>

If we ran the application now we would see just some empty boxes with some errors in the console, like so:

Child Component Error

Because product property of ProductComponent is not initialized yet as well as we are not passing product data from our parent ProductsComponent to the child ProductComponent.

To send product data in ProductComponent, from parent component we need to use Property Binding.

As you have seen in above code, In ProductComponent class we do have a property called product:

And we are looping and creating products in our ProductsComponent, like so:

<app-product class="col-md-3" *ngFor="let p of products"></app-product>

But we are not setting the property product of our ProductComponent to anything, which is why it’s undefined.

<app-product class="col-md-3" [product]="p" *ngFor="let p of products"></app-product>

Even though our ProductComponent has a product property we can’t bind to it using the [] syntax, we need to explicitly mark it as an Input property on our ProductComponent.

We do this by pre-pending the product property in the component with a new annotation called @Input, as below :

import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
class ProductComponent{
   @Input() product: Product;
   ...
}

This tells Angular that the product property is an input property and therefore in HTML we can bind to it using the [] input property binding syntax.

This @Input now becomes part of the public interface of our component.

Lets say at some future point we decided to change the product property of our ProductComponent to perhaps just data, like so:

class ProductComponent {
  @Input() data: Product;
  ...
}

Because this input is part of the public interface for our component we would also need to change all the input property bindings everywhere our component is used, like so:

<app-product class="col-md-3" [data]="p"  *ngFor="let p of products"></app-product>

Not a great thing to ask the consumers of your component to have to do.

This is a common problem so to avoid expensive refactors the @Input annotation takes a parameter which is the name of the input property to the outside world, so if we changed our component like so:

class ProductComponent{
   @Input('product') data: Product;
   ...
}

To the outside world, the input property name is still product and we could keep the ProductsComponent template the same as before:

<app-product class="col-md-3" [product]="p"  *ngFor="let p of products"></app-product>

Now once browser refresh, the output would be like :

Property Binding and Input Property
Property Binding and Input Property

To send an input to the child component from the parent component we have use the @Input directive.

But what if child component generates a custom event and it needs to inform this custom event to the parent component.

To create a custom event on child component we need two things,

  • Create an EventEmitter property on the child component called ProductComponent.
  • Similar to input property binding, we need to annotate that property with the @Output decorator.

An EventEmitter is a helper class which we can use to emit events when something happens, other components can then bind and react to these events.

Create one Remove button on ProductComponent template. as shown below : 

<button type="button" class="btn btn-danger"> Remove </button>

We will create one event called removed in ProductComponent class. 

import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
class ProductComponent{
  @Output() removed = new EventEmitter();
  ...
  delete() {
    this.removed.emit(this.product);
  }
  ...
}

As you can see above, I have created one removed property which will generate an event. I have created one delete() method which is a click event handler of Remove button.

Once you click on the  Remove button this will call the delete() method. In delete() method we will emit an event using removed property. We will send the product object which we want to remove as a parameter of emit() method. as shown,

this.removed.emit(this.product);

We need to handle this output removed event on our parent component.

That we can do by adding (removed)="deleteProduct($event)" in the <app-product> </app-product>  tag of an products.component.html

As shown below.

<app-product class="col-md-3" [product]="p" (removed)="deleteProduct($event)" *ngFor="let p of products"></app-product>

A $event is the object which we have received from the child component called ProductComponent.

As you can see above, we receive the removed event here, but to handle this event in ProductsComponent we need to create deleteProduct() method. as shown

class ProductsComponent{
  ...
  deleteProduct(product: Product) {
      let index = this.products.indexOf(product);
      if (index !== -1) {
        this.products.splice(index, 1);
      }
   }
}

let is used to create a local variable inside the method or constructor. For example let temp=5;

This method first check the product is available or not in the list using indexOf() of an array.

If indexOf() method return -1 then product is not available. Otherwise it will return the index of the element.

Once we find the index, we can remove the element from an array using splice() method of an array. In splice method we need to pass the index from which we want to remove, and then no. of elements we want to remove, as show above splice(index,1) method will remove one element from given index.

import { MockData } from './../mock-data/mock-product-data';
import { Component, OnInit } from '@angular/core';
import { Product } from '../models/product';

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {

  products: Product[] = [];

  constructor() {
    this.products = MockData.Products;
  }

  ngOnInit() {
  }

  deleteProduct(product: Product) {
    let index = this.products.indexOf(product);
    if (index !== -1) {
      this.products.splice(index, 1);
    }
  }

}
<div class="card" >
  <div class="card-body">
    <h4 class="card-title">Products</h4>
    <div class="row">
      <app-product class="col-md-3" [product]="p" (removed)="deleteProduct($event)" *ngFor="let p of products"></app-product>
    </div>  
  </div>
</div>
import { Product } from './../models/product';
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {

  @Input() product: Product;
  @Output() removed = new EventEmitter<Product>();

  constructor() { }

  ngOnInit() {
  }

  delete() {
    this.removed.emit(this.product);
  }

}
<div class="card bg-secondary text-white">
  <div class="card-body">
    <h4 class="card-title">{{product.title}}</h4>
    <h5 class="card-text">{{product.price|currency:'INR'}}</h5>
  </div>
  <table class="table">
    <tr>
      <td scope="row">
        <b>Product Type</b>
      </td>
      <td>
        <i class="fa fa-laptop fa-2x" aria-hidden="true" *ngIf="product.productType=='Laptop'"></i>
        <i class="fa fa-mobile fa-2x" *ngIf="product.productType=='Mobile'" aria-hidden="true"></i>
      </td>
    </tr>
    <tr>
      <td scope="row">
        <b>Brand</b>
      </td>
      <td>{{product.brand}}</td>
    </tr>
  </table>
  <div class="card-footer text-muted">
    <button type="button" class="btn btn-danger" (click)="delete()" >Remove</button>
  </div>
</div>

In this chapter,

  • We have seen the purpose of nested component, and then we have created nested component.
  • We have created one nested component called ProductComponent.
  • After that, we have seen the way to communicate between parent component to child component.
  • We can pass the data from parent component to child component using @Input Directive in child component with Property Binding in parent component.
  • As well as, to emit an event from child component we need to use @Output Directive.