Angular - Working with components hierarchy
Jose C
Posted on January 4, 2022
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 ondumb components
. They are also calledapplication-level-components
,container components
orcontrollers
.Dumb components
: Their only responsability is to show information or execute functions from thesmart component
. Also calledpresentation components
orpure 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 component
is 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 component
will be responsible to loop the brands list and use the list-element
component 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.
Posted on January 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.