Angular - Working with components hierarchy

On Angular and other frontend frameworks or libraries like React or Next we work by creating components. This components allows us to:

  • Separate responsibilities.
  • Re-use code.
  • Makes coding easier.
  • Facilitates maintenance.

In order to achieve what I mentioned above we have to start thinking about some things before we start coding:

  • How many components do I need?
  • Which will be his responsibility?
  • Can I reuse it?

Based on components duties we can sort components into 2 groups:

  • Smart components: Keep all functions and are responsible for getting all the information shown on dumb components. They are also called application-level-components, container components or controllers.

  • Dumb components: Their only responsability is to show information or execute functions from the smart component. Also called presentation components or pure components.

Ok this is the theory but let’s see one example of smart and dumb components.

Components hierarchy in action

To start I will create a new angular app:

ng new angular-hierarchy-components --style=scss --routing=true --skipTests=true

I will create a very basic app that is just a list and a form and buttons to add and remove elements to that list. At first I will do everything on the app.component to later refactor it using smart and dumb components.

This is all my code on the app.component.ts and app.component.html:

app.component.ts:

export class AppComponent {
  title = 'angular-hierarchy-components';

  brands: string[] = [`Mercedes`, `Ferrari`, `Porsche`, `Volvo`, `Saab`];

  remove(id: number) {
    this.brands.splice(id, 1);
  }

  new(brand) {
    this.brands.push(brand.value);
  }
}

All I have is a brands list and 2 functions remove to remove brands from the list and new to add new brands to the list.

And this is the app.component.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <div class="container__form">
        <form #root="ngForm" (ngSubmit)="new(newBrand)">
          <input type="text" name="brand" #newBrand />
          <button type="submit" #sendButton>Add</button>
        </form>
      </div>
      <div class="container__brand" *ngFor="let brand of brands; let i = index">
        <div class="container__brand__name">
          {{ brand }}
        </div>
        <div class="container__brand__button">
          <input type="button" (click)="remove(i)" value="x" />
        </div>
      </div>
    </div>
  </body>
</html>

I have a form that when on submit runs the new function that adds a new brand to the brands list and a ngFor that prints each brand name and a button to execute the remove function that removes the brand from the list.

This code works perfectly but I see some weakness on init:

  • There’s no way to reuse the code that prints out the brand list and the button to remove the brands name. If I want to implement this functionality on the same app but for clothing brands I will have to repeat the code.

  • If the app keeps growing I will have to stack all the functionalities on the app.component.ts so after adding each functionality the app turns out to be more and more difficult to maintain.

To solve he points I mentioned above I will split my code on smart and dumb components.

I will start by creating the smart component that will contain:

  • The brands list.
  • The new method to add new brands to the list.
  • The remove method that removes brands from the list.

Splitting my code to smart and dumb components

Creating the smart component

To solve he points I mentioned above I will split my code on smart and dumb components.

I will start by creating the smart component that will contain:

  • The brands list.
  • The new method to add new brands to the list.
  • The remove method that removes brands from the list.

On the terminal I create the smart component as a regular one:

ng generate component smartComponent

Usually I create the smart components to use as pages so I name it like blogPage or something like that but for this case I will just call it smartComponent.

On this component I will move the code I had on my app.component.ts to smart-component.ts so now it will look like this:

export class SmartComponentComponent implements OnInit {
  constructor() {}

  ngOnInit(): void {}

  brands: string[] = [`Mercedes`, `Ferrari`, `Porsche`, `Volvo`, `Saab`];

  remove(id: number) {
    this.brands.splice(id, 1);
  }

  new(brand: string) {
    this.brands.push(brand);
  }
}

Nothing new yet.

Now I will have to remove the default content on the smart-component.component.html and set the layout to render the dumb components and I will have to create two dumb components:

  • One component for the form to add new brands.
  • Another to render the brands name and the remove button.

This is the layout:

<div class="container">
  <div class="container__form">
   <!-- here goes the brands form -->
  </div>
  <div class="container__brand" *ngFor="let brand of brands; let i = index">
   <!-- Here goes the brands name component -->
  </div>
</div>

Creating the dumb components

Creating the list-element component

Now let’s go to the dumb components.

First I will create the list-element components. This component will render one brand’s name and a button close to it to remove the brand from the list.

I create the component as a regular one:

ng generate component listElement

Now on the list-element.component.ts I have to define:

  • The brand 's name.
  • The brand’s id (actually the position on the brands name array).

But wait, we did not agree that the brands array and all the information was on the smart component? Yes. The smart component will hold all the information and functions but will pass the brand’s name and array position to the dumb component in our case list-element using angular input binding.

To do that we first have to import Input from @angular/core on the list-element.component.ts component:

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

Now we can use the @Import() decorator to define the values we are expecting:

@Input() brand: string;
@Input() id: number;

This way we are telling our component that he’s going to receive the brand's name and id (actually array position on the smart component).

Now let’s render the name and a button on the list-element.component.ts:

<div class="container__brand">
  <div class="container__brand__name">
    {{ brand }}
  </div>
  <div class="container__brand__button">
    <input type="button" value="x" />
  </div>
</div>

This way we can render the name and a button on the screen.

Now on this same component we have to implement a method that allows us to execute the remove method we have on the smart component.

To execute the remove function we defined on the smart component from the list-element component we have to use another functionality from angular called Output in conjunction with EventEmitter. This will allow us to “emit” events to the smart component in order to execute methods.

First let’s add the Output and EventEmitter to our import:

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

Now I can use the @Output decorator and the EventEmitter:

@Output() removeEvent = new EventEmitter<number>();

And in my list-element.component.ts I will define a method that will trigger the EventEmitter when the user clicks on the remove button:

removeBrand(id: number) {
    this.removeEvent.emit(id);
}

This method will receive the array position of the brand and emit it to the smart component so the remove method on the smart componentis executed and the brand is removed from the list.

Now on the element-list.component.html we have to implement this method when the user clicks the remove button:

<div class="container__brand">
  <div class="container__brand__name">
    {{ brand }}
  </div>
  <div class="container__brand__button">
    <input type="button" (click)="removeBrand(id)" value="x" />
  </div>
</div>

Ok, now let's connect the smart component with the element-list component. The smart componentwill be responsible to loop the brands list and use the list-elementcomponent to render the brands name and a button to remove. On the smart-component.html we will use the element-list component and pass to it the brands name and array position:

<div class="container">
  <div class="container__form">
   <!-- here goes the new brand form component -->
  </div>
  <div class="container__brand" *ngFor="let brand of brands; let i = index">
    <app-list-element
      [brand]="brand"
      [id]="i"
      (removeEvent)="remove($event)"
    ></app-list-element>
  </div>
</div>

Let’s take a look at the app-list-element component tag. We can see we are using 3 parameters/attributes:

  • brand: it’s the brand’s name.
  • id: the array position for the brand.
  • (removeEvent): it’s the removing brand event.

brand and id uses [] and events uses () it’s the same we do in Angular when we use data-binding or any other event like click:

  • For binding data between components: [data].
  • For binding events: (event).

Ok we’re done with this now let’s go for the new brands form.

Creating the new brand component

First we create the new brand form component:

ng generate component newBrand

This component will only contain the new brand form and emit the new brand’s name to the smart component so I will start by importing Output and EventEmitter to emit the new value:

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

And define the new EventEmitter in the component using the @Output decorator:

@Output() newEvent = new EventEmitter<string>();

And define a new method that will emit the new brand’s name to the smart component:

new(brand: { value: string; }) {
    this.newEvent.emit(brand.value);
  }

And on the new-brand.component.html I add the form and set it to execute the new method when submit:

<form #newBrand="ngForm" (ngSubmit)="new(newBrandInput)">
    <input type="text" name="brand" #newBrandInput />
    <button type="submit" #sendButton>Add</button>
</form>

Now we only have to connect the smart component to the new-brand component on the smart-component.component.html:

<div class="container">
  <div class="container__form">
    <app-new-brand (newEvent)="new($event)"></app-new-brand>
  </div>
  <div class="container__brand" *ngFor="let brand of brands; let i = index">
    <app-list-element
      [brand]="brand"
      [id]="i"
      (removeEvent)="remove($event)"
    ></app-list-element>
  </div>
</div>

On the new-brand tag component I have defined an event called newEvent and binded to the new method on smart-component.component.ts.

And that's all.

Here you can find a repository with 2 branches: first one without components hierarchy and a second one with the component hierarchy I showed you on this post.

31