38
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:
In order to achieve what I mentioned above we have to start thinking about some things before we start coding:
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.
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:new
method to add new brands to the list.remove
method that removes brands from the list.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:new
method to add new brands to the list.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
: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>
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: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
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
:Ok we’re done with this now let’s go for the new brands form.
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.
38