Home » Posts » Inter component communication in Angular

Inter component communication in Angular

Before we start talking about how inter component communication is achieved in Angular, we should first understand what inter component communication is.

Consider a situation where we have multiple components on our page and when a user interacts with a component, other components might have to be re-rendered based on what changes happened in the first component. To achieve this, the component which was changed or interacted upon needs to somehow inform other components to re-render themselves. This is called inter component communication.

Let’s take an example of a simple note taking app which looks something like below.

A simple note taking app

Here, the left section, where we are adding a new note and the right section, where the notes are displayed are two angular components which are placed in app component. This means, app component is the parent and the two components are child of the app component and siblings to each other.

What are the different ways to achieve inter component communication?

There are two ways to achieve this – through events and through service. Let’s understand both for the above example only.

Through Events

Event based communication

The component which is interacted upon raises appropriate events with or without data to its parent which in turn modifies some model/state/data which are provided as input to other components. These components then re-render themselves according to the changes happened in their @Input properties. Note that, the changes in input properties can either be directly reflected in the view or can be processed and acted upon through ngOnChanges lifecycle method.

Let’s understand this through an example.

app.component.html

  <div class="new-note">
      <app-new-note-event (onNewNoteAddition)="handleNewNoteAddition($event)"></app-new-note-event>
    </div>

    <div class="note-list">
      <app-note-list-event [notes]="notes"></app-note-list-event>
    </div>
  </div>

Here, we can see that a new note component is raising an event which is handled by the app component and note list component takes notes as an input property.

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  public title = 'inter-component-communication';
  public notes: any[] = [];

  public handleNewNoteAddition(note: any): void {
    this.notes.push(note);
  }
}

Here, we see the app component handling the event raised by the new note component.

New note component

<div class="note-event-container">
    <div class="error" *ngIf="showErrorMessage"><span>Note title and description can't be left empty</span></div>

    <div><input type="text" id="note-title" placeholder="Enter note title" [(ngModel)]="noteTitle"></div>

    <div><textarea name="note-description" id="note-description" cols="30" rows="10" placeholder="Enter note"
            [(ngModel)]="noteDescription"></textarea></div>

    <div class="note-event-footer" (click)="add()"><button>Add</button></div>
</div>
import { Component, EventEmitter, OnInit, Output } from '@angular/core';

@Component({
  selector: 'app-new-note-event',
  templateUrl: './new-note-event.component.html',
  styleUrls: ['./new-note-event.component.scss']
})
export class NewNoteEventComponent implements OnInit {
  public noteTitle: string = '';
  public noteDescription: string = '';
  public showErrorMessage: boolean = false;

  @Output() onNewNoteAddition: EventEmitter<object> = new EventEmitter<object>();

  constructor() { }

  ngOnInit(): void {
  }

  private areInputValid() {
    return this.noteTitle.trim() !== '' && this.noteDescription.trim() !== '';
  }

  public add() {
    if (!this.areInputValid()) {
      this.showErrorMessage = true;
      return;
    }

    this.showErrorMessage = false;
    this.onNewNoteAddition.emit({ title: this.noteTitle.trim(), description: this.noteDescription.trim() });
    this.clear();
  }

  private clear() {
    this.noteTitle = '';
    this.noteDescription = '';
  }

}

Above code shows the new note component raising an event when a new note is added.

Note list component

<div class="note-list-container">
    <div class="no-notes" *ngIf="notes.length == 0"><span>No notes available</span></div>

    <div class="note" *ngFor="let note of notes">
        <h3>{{note.title}}</h3>

        <p>{{note.description}}</p>
    </div>
</div>
import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-note-list-event',
  templateUrl: './note-list-event.component.html',
  styleUrls: ['./note-list-event.component.scss']
})
export class NoteListEventComponent implements OnInit {

  @Input() notes: any[] = [];

  constructor() { }

  ngOnInit(): void {
  }

}

Note list component having [notes] as an input property which when changed re-renders the view of this component.

Through service

Service based communication

In this scenario, we follow a publisher-subscriber (pub-sub) pattern where one component publishes some event to a subject and other component listens/subscribes to that subject. Subscribing components re-renders themselves basis what event has been published.

Let’s use this method in the above example.

app.component.html

  <div class="service-demo">
    <div class="new-note">
      <app-new-note-service></app-new-note-service>
    </div>

    <div class="note-list">
      <app-note-list-service></app-note-list-service>
    </div>
  </div>

In this case, we just add the components to the app component without any events or properties.

Notes service

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NotesService {

  private notesSource: Subject<any> = new Subject<any>();

  public notesSource$: Observable<any> = this.notesSource.asObservable();

  constructor() { }

  public addNote(note: any): void {
    this.notesSource.next(note);
  }
}

The above code shows how a subject is created and an observable is created to which components can subscribe. Here we also create a method which publishes an event/data to all the subscribers.

New note component

<div class="note-event-container">
    <div class="error" *ngIf="showErrorMessage"><span>Note title and description can't be left empty</span></div>

    <div><input type="text" id="note-title" placeholder="Enter note title" [(ngModel)]="noteTitle"></div>

    <div><textarea name="note-description" id="note-description" cols="30" rows="10" placeholder="Enter note"
            [(ngModel)]="noteDescription"></textarea></div>

    <div class="note-event-footer" (click)="add()"><button>Add</button></div>
</div>
import { Component, OnInit } from '@angular/core';
import { NotesService } from '../notes.service';

@Component({
  selector: 'app-new-note-service',
  templateUrl: './new-note-service.component.html',
  styleUrls: ['./new-note-service.component.scss']
})
export class NewNoteServiceComponent implements OnInit {
  public noteTitle: string = '';
  public noteDescription: string = '';
  public showErrorMessage: boolean = false;

  constructor(private notesService: NotesService) { }

  ngOnInit(): void {
  }

  private areInputValid() {
    return this.noteTitle.trim() !== '' && this.noteDescription.trim() !== '';
  }

  public add() {
    if (!this.areInputValid()) {
      this.showErrorMessage = true;
      return;
    }

    this.showErrorMessage = false;
    this.notesService.addNote({ title: this.noteTitle.trim(), description: this.noteDescription.trim() });
    this.clear();
  }

  private clear() {
    this.noteTitle = '';
    this.noteDescription = '';
  }

}

Unlike communicating through events, here, a new note component is publishing a new note through the notes service.

Note list component

<div class="note-list-container">
    <div class="no-notes" *ngIf="notes.length == 0"><span>No notes available</span></div>

    <div class="note" *ngFor="let note of notes">
        <h3>{{note.title}}</h3>

        <p>{{note.description}}</p>
    </div>
</div>
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { NotesService } from '../notes.service';

@Component({
  selector: 'app-note-list-service',
  templateUrl: './note-list-service.component.html',
  styleUrls: ['./note-list-service.component.scss']
})
export class NoteListServiceComponent implements OnInit, OnDestroy {
  private notesSubscription: Subscription | undefined;

  public notes: any[] = [];

  constructor(private notesService: NotesService) { }

  ngOnInit(): void {
    this.notesSubscription = this.notesService.notesSource$.subscribe((note: any) => {
      this.notes.push(note);
    });
  }

  ngOnDestroy(): void {
    if (this.notesSubscription) {
      this.notesSubscription.unsubscribe();
    }
  }

}

Here, the note list component subscribes to an observable and re-renders the view when a new note is published.

When to use what?

Well, it depends on your use case and you should assess what works best for you but as a rule of thumb, you can use the event way in following scenarios.

  • If a few components are placed in a view as siblings i.e., placed side by side.
  • If level of nesting is around 2 or 3 (maximum) in case components are placed in a parent child relationship.

And, you can use the service way in the following cases.

  • A lot of different components are placed in a view as siblings.
  • Level of nesting is quite large (>= 3) when components are placed in a parent child relationship.

Parent calling child component’s methods through @ViewChild

In nested component scenario, there is a way through which parent component can call child component’s methods. That way is through @ViewChild property decoration. We haven’t discussed it here because most of the uses can be dealt with without this but if you want to read more about it, you can do it here.

References