Communication between components in Angular applications
What is a component?
Components let you split the UI into independent, reusable pieces, and think about each piece in isolation. Every component contains a template, the styling and some bussines logic represented by a class.
In Angular we distinguish several ways of communication between components: parent to child, child to parent and any to any.
Parent to child:
Pass data using @Input() decorator:
Consider the parent component having some data that need to be sent to the child component. Let's say this parent has a list of football teams and we want to pass some information to the child. We can accept the data from the parent component by using @Input() decorator inside the child component.
"team.component.ts" (Child component)
import { Component, Input } from '@angular/core';
import { Team } from '../../shared/interfaces/index.ts';
@Component({
selector: 'app-team',
template: `
<h3>{{ team.name }}</h3>
<p>{{ team.position }} - {{ team.points}}</p>
`
})
export class TeamComponent {
@Input() team: Team;
constructor() {}
}
The "TeamsListComponent" nests the "TeamComponent" inside an *ngFor repeater, binding each iteration's team instance to the child's team property.
"teams-list.component.ts" (Parent component)
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { TeamsService } from '../shared/services/index.ts';
import { Team } from '../shared/interfaces/index.ts';
@Component({
selector: 'app-teams-list',
template: `
<div *ngFor="let team of teams" class="team" fxLayout="row wrap" fxLayoutAlign="space-around center">
<app-team [team]="team"></app-team>
</div>
`
})
export class TeamsListComponent implements OnInit, OnDestroy {
teams: Team[] = [];
subscription: Subscription;
constructor(private teamsService: TeamsService) {}
ngOnInit(): void {
this.fetchTeamsData();
}
private fetchTeamsData() {
this.subscription = this.teamsService.getTeams().subscribe(teams => this.teams = teams);
}
ngOnDestroy() {
// prevent memory leak when component destroyed
this.subscription.unsubscribe();
}
}
Child to Parent:
Send Event with Output() decorator:
The @Output() annotation allows us to achieve the opposite result, pass values from child to parent by using the EventEmitter. Let's say that we have a small app in which we want to vote if we agree/disagree with an statement.
"voter.component.ts" (Child component)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-voter',
template: `
<h4>Voter: {{name}}</h4>
<button (click)="vote(true)" [disabled]="didVote">Agree</button>
<button (click)="vote(false)" [disabled]="didVote">Disagree</button>
`
})
export class VoterComponent {
@Input() name: string;
@Output() voted = new EventEmitter<boolean>();
didVote = false;
vote(agreed: boolean) {
this.voted.emit(agreed);
this.didVote = true;
}
}
The child component exposes an EventEmitter property with which it emits events when something happens. The parent binds to that event property and reacts to those events.
We can capture an emitted event in parent component inside the template like the example below:
"vote-taker.component.ts" (Parent component)
import { Component } from '@angular/core';
@Component({
selector: 'app-vote-taker',
template: `
<h2>Should we keep learning Angular?</h2>
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
<app-voter *ngFor="let voter of voters"
[name]="voter"
(voted)="onVoted($event)">
</app-voter>
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Agustin', 'Thomas', 'Marta', 'Julia'];
onVoted(agreed: boolean) {
agreed ? this.agreed++ : this.disagreed++;
}
}
Parent calls @ViewChild():
The Angular @ViewChild() decorator is one of the most commonly used decorators.
ViewChild allows one component to be injected into another, giving the parent access to its attributes and functions. However, the child won’t be available until after the view has been initialized. This means we need to implement the AfterViewInit lifecycle hook to receive the data from the child.
From the parent component template, we can create a template reference using #. And with the @ViewChild template reference, we can access the child component properties.
"parent.component.ts"
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child/child.component';
@Component({
selector: 'app-parent',
template: `
Message: {{ message }}
<app-child #child></app-child>
`,
styleUrls: ['./parent.component.css']
})
export class ParentComponent implements AfterViewInit {
@ViewChild('child') child: ChildComponent;
message: string;
ngAfterViewInit() {
this.message = this.child.message
}
}
"child.component.ts"
import { Component } from '@angular/core';
@Component({
selector: 'app-child',
template: `
`,
styleUrls: ['./child.component.css']
})
export class ChildComponent {
message = 'Hello world from LinkedIn!';
constructor() { }
}
There are some scenearios in which this decorator can be really useful such as DOM manipulations similar to what we used to do with jQuery to change the native HTML element. Another example in which we have to use Viewchild is when working with Angular Material components.
Let's see a basic example of DOM manipulation with the ViewChild decorator:
import { Component, ViewChild, AfterViewInit, ElementRef } from '@angular/core';
@Component({
selector: 'my-input',
template: `
<input type="text" [(ngModel)]="name" #nameRef />
`,
styleUrls: [ './input.component.css' ]
})
export class InputComponent implements AfterViewInit {
name: string;
@ViewChild('nameRef') nameReference: ElementRef;
ngAfterViewInit() {
this.nameReference.nativeElement.focus();
}
}
After the view is initialized the input element it's going to be focused by default.
Share Data Between Any Components Using Service:
If you are used to work with Angular you may encountered the situation in which you have a parent component with two or more childs and you need to share data between all of them. Imagine that our main component has one attribute of type number which stores the age of a person. This age needs to be modified in one of the child components and at the same time it needs to be shown in the other component. A normal way of dealing with this stage if you don't have any clue about reactive programming and services could be send the variable from the parent to the child with the @Input() decorator. Once this is done you can update the age within the child component and emit an event back to the parent with the current value. Finally you can bind the updated age to the other child component with @Input() one more time and display the person age in the template.
This is a valid solution but we will have to create inputs/outputs for every component that needs to interacts with this data flow. Luckily there is a better way to handle this kind of situations in which we have multiple components that requires the same data. Let's get into that.
When passing data between components that lack a direct connection, such as siblings components, you should use a shared service. Since we need to have all the data synchronised between components we use something call Observables. What is that? Well, an observable is a communication channel that emits data that can be listen by observers that are subscribed to it. When a value is emitted, the component that is observing detect the changes and reacts to them. All this comes from an important concept called Reactive Programming, in which basically the components react to the data flow in our application. It's really good to use reactive programming libraries such as RxJS.
There are two ways of using observables: Subject & BehaviorSubject
A Subject is a type of Observable that allows values to be multicasted to many Observers. While plain Observables are unicast. When an observer is subscribed to this Subject once the information was already sent, we will lose this information.
If you want to keep that information, it might be useful to use BehaviorSubject that contains a buffer that holds the last value. So, when it is subscribed it emits the value immediately. This is really good for storing states in our application.
Example:
In the service, we create a private BehaviorSubject that will hold the current value of a message. We define a currentMessage variable that will handle this data stream as an observable that will be used by the components. Lastly, we create function that calls next on the BehaviorSubject to change its value.
All the components receive the same treatment. We inject the DataService in the constructor, then subscribe to the currentMessage observable and set its value equal to the message variable.
Now if we create in any one of these components a function that changes the value of the message, once this function is executed the new data it’s automatically broadcast to all components.
"data.service.ts"
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class DataService {
private messageSource = new BehaviorSubject('Default message.');
currentMessage = this.messageSource.asObservable();
constructor() { }
changeMessage(message: string) {
this.messageSource.next(message);
}
}
"first-sibling.component.ts"
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService } from "../data.service";
@Component({
selector: 'app-first-sibling',
template: `
<p>{{message}}</p>
`,
styleUrls: ['./sibling.component.css']
})
export class FirstSiblingComponent implements OnInit, OnDestroy {
message: string;
subscription: Subscription;
constructor(private data: DataService) { }
ngOnInit() {
this.subscription = this.data.currentMessage.subscribe(message => this.message = message);
}
ngOnDestroy() {
// prevent memory leak when component destroyed
this.subscription.unsubscribe();
}
}
"second-sibling.component.ts"
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService } from "../data.service";
@Component({
selector: 'app-second-sibling',
template: `
<p>{{message}}</p>
<button (click)="newMessage()">New Message</button>
`,
styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit, OnDestroy {
message:string;
subscription: Subscription;
constructor(private data: DataService) {}
ngOnInit() {
this.subscription = this.data.currentMessage.subscribe(message => this.message = message);
}
newMessage() {
this.data.changeMessage("Hello from Sibling");
}
ngOnDestroy() {
// prevent memory leak when component destroyed
this.subscription.unsubscribe();
}
}