Author avatar

Chris Parker

Communicating across Components Using Services

Chris Parker

  • May 9, 2019
  • 16 Min read
  • 29 Views
  • May 9, 2019
  • 16 Min read
  • 29 Views
Languages Frameworks and Tools
Angular

Introduction

In any Angular app, components are the basic building blocks. Angular Components provide us with the views/templates for the app. They also manage the data bound to those views. Below are some of the most common data sharing scenarios in any app, where two or more components share information between them:

  1. Parent Component to Child Component: Using @Input decorator (property binding)
  2. Child Component to Parent Component: Using @Output decorator and EventEmitter (event binding)
  3. Between Sibling or Unrelated Components: Using the Angular Shared Service

Let us have a look at each of these scenarios with examples.

Setup for Our Example

In our example, we will display a list of courses to the user and, upon selecting any course, the user will be taken to the details for that course.

Below is how our app.module.ts would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { CourseListComponent } from './courses/course-list/course-list.component';
import { CourseItemComponent } from './courses/course-list/course-item/course-item.component';

@NgModule({
  declarations: [
    AppComponent,
    CourseListComponent,
    CourseItemComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
javascript

Below is how our course model would look like:

1
2
3
4
5
6
7
8
9
10
11
12
//course.model.ts
export class Course {
  public name: string;
  public description: string;
  public courseImagePath: string;

  constructor(name: string, desc: string, courseImagePath: string) {
    this.name = name;
    this.description = desc;
    this.courseImagePath = imagePath;
  }
}
javascript

Our root component, or the app component, contains the course-list component and looks like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit } from '@angular/core';
import { Course } from './course.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  selectedCourse: Course;
  constructor() { }
  ngOnInit() {
  }
}
javascript

Below is the HTML code for the app component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="row">
  <div class="col-md-5">
<app-course-list
  (courseWasSelected)="selectedCourse = $event"></app-course-list>
  </div>
  <div class="col-md-7">
<app-course-detail
  *ngIf="selectedCourse; else infoText"
  [course]="selectedCourse"></app-course-detail>
<ng-template #infoText>
  <p>Please select any course to view details!</p>
</ng-template>
  </div>
</div>
html

Also, our course-list component looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
import { Course } from '../course.model';

@Component({
  selector: 'app-course-list',
  templateUrl: './course-list.component.html',
  styleUrls: ['./course-list.component.css']
})
export class CourseListComponent implements OnInit {
  courses: Course[] = [
    new Course('Angular Course 1', 'This is simply a practice Angular course 1', 'http://via.digital.com/350x150.jpeg'),
    new Course('Angular Course 2', 'This is simply a practice Angular course 2', 'http://via.digital.com/350x150'),
	new Course('Angular Course 3', 'This is simply a practice Angular course 3', 'http://via.digital.com/350x150')
  ];

  constructor() { }

  ngOnInit() {
  }
}
javascript

Below is the template:

1
2
3
4
5
6
7
8
9
10
11
<div class="row">
  <div class="col-xs-12">
    <button class="btn btn-success">New Course</button>
  </div>
</div>
<hr>
<div class="row">
  <div class="col-xs-12">
    <app-course-item></app-course-item>
  </div>
</div>
html

Below is our course item component:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

import { Course } from '../../course.model';

@Component({
  selector: 'app-course-item',
  templateUrl: './course-item.component.html',
  styleUrls: ['./course-item.component.css']
})
export class CourseItemComponent implements OnInit {  
  constructor() { }
  ngOnInit() {  }
}
javascript

And the course item template:

1
2
3
4
5
6
<a
  href="#"
  class="list-group-item clearfix">
  <div class="pull-left"><!--Display Course Name/Desc here --></div>
  <span class="pull-right"><!--Display Course Image here--></span>
</a>
html

With the above setup, let us have a look at the different ways to communicate between these components.

Using @Input decorator (Property Binding)

This is the most common way of sharing data. It uses the @Input() decorator to pass data via the template. @Input decorator allows parent component to bind it's properties to child component and thus gives the child component access to its data. These bindings are actually a reference to properties on the parent component.

Say we want to pass "course" information from our CourseListComponent to the CourseItemComponent.

Below is how we would update our course-list template:

1
2
3
4
5
6
7
<div class="row">
  <div class="col-xs-12">
    <app-course-item
      *ngFor="let courseEl of courses"
      [course]="courseEl"></app-course-item>
  </div>
</div>
html

You can see that, as we are iterating over the list of courses, we pass the individual course element for that iteration to the CourseItemComponent. Thus, we are binding the "course" property of the child CourseItemComponent from the parent CourseListComponent.

Now, to be able to do this, we have to use the @Input decorator on the child CourseItemComponent. Thus, our updated CourseItemComponent would look like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

import { Course } from '../../course.model';

@Component({
  selector: 'app-course-item',
  templateUrl: './course-item.component.html',
  styleUrls: ['./course-item.component.css']
})
export class CourseItemComponent implements OnInit {
@Input() course: Course;  
  constructor() { }
  ngOnInit() {  }
}
javascript

We can now render the course name/description/image in the CourseItemComponent template as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<a
  href="#"
  class="list-group-item clearfix">
  <div class="pull-left">
    <h4 class="list-group-item-heading">{{ course.name }}</h4>
    <p class="list-group-item-text">{{ course.description }}</p>
  </div>
  <span class="pull-right">
        <img
          [src]="course.courseImagePath"
          alt="{{ course.name }}"
          class="img-responsive"
          style="max-height: 50px;">
      </span>
</a> 
html

Using @Output Decorator and EventEmitter (Event Binding)

In certain cases, we want to be able to emit data back from the child component to the parent component. There is an EventEmitter property exposed by the Child component exposes, which would emit data whenever any action/event occurs on the child component.

In the same example we looked at above, let's say that whenever any of the individual course’s link is clicked, we want to inform the parent component about the same.

So, we'll have a "click" event on the CourseItemComponent. This is how the template for that would be updated to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<a
  href="#"
  class="list-group-item clearfix"
	(click)="onSelected()">
  <div class="pull-left">
    <h4 class="list-group-item-heading">{{ course.name }}</h4>
    <p class="list-group-item-text">{{ course.description }}</p>
  </div>
  <span class="pull-right">
        <img
          [src]="course.courseImagePath"
          alt="{{ course.name }}"
          class="img-responsive"
          style="max-height: 50px;">
      </span>
</a>
html

Now, from our CourseItemComponent typescript code, we want to be able to emit this event and inform the parent component about the event. If we want "courseSelected" to be listenable from outside, we have to use the @Output decorator. This would give access to emit() method on this property. Also while creating an instance of EventEmitter, we can optionally define any event data that we might emit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

import { Course } from '../../course.model';

@Component({
  selector: 'app-course-item',
  templateUrl: './course-item.component.html',
  styleUrls: ['./course-item.component.css']
})
export class CourseItemComponent implements OnInit {
@Input() course: Course;  
@Output() courseSelected = new EventEmitter<Course>();
  constructor() { }
  ngOnInit() {  }
onSelected() {
    this.courseSelected.emit(this.course);
  }
}
javascript

With the above change, we would now be able to listen to the "courseSelected" event on the parent CourseListComponent as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="row">
  <div class="col-xs-12">
    <button class="btn btn-success">New Course</button>
  </div>
</div>
<hr>
<div class="row">
  <div class="col-xs-12">
    <app-course-item
      *ngFor="let courseEl of courses"
      [course]="courseEl"
      (courseSelected)="onCourseSelected(courseEl)"></app-course-item>
  </div>
</div>
html

Also, we'll now get access to the "course" in our CourseListComponent typescript code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
import { Course } from '../course.model';

@Component({
  selector: 'app-course-list',
  templateUrl: './course-list.component.html',
  styleUrls: ['./course-list.component.css']
})
export class CourseListComponent implements OnInit {
  courses: Course[] = [
    new Course('Angular Course 1', 'This is simply a practice Angular course 1', 'http://via.digital.com/350x150.jpeg'),
    new Course('Angular Course 2', 'This is simply a practice Angular course 2', 'http://via.digital.com/350x150')
  ];

  constructor() { }

  ngOnInit() {
  }

onCourseSelected(course: Course) {
   // console.log("Selected course : " + course);
  }
}
javascript

Thus, we see that the parent component binds to the event which is outputted by our child component via the template. Also, the parent component reacts to this event using the handler function. The value which gets emitted by the child component is passed to the parent's handler function. Generally, we use $event to refer to any event data.

Using the Angular Shared Service

Sharing data between components using property binding/event binding with the help of @Input/@Output decorators helps in making our components independent. This strategy makes our components easily reusable and can be tested easily.

However, as our application grows and our components get more complex with deep nesting over time, it then becomes difficult to manage the data flow using these @Input/@Output decorators.

In such scenarios, it is more efficient to pass data around between components using a shared service. A service is simply an Angular class that acts as a central repository.

Let us update our example to use such a shared service to pass data, instead of using property/event binding.

We'll create a new service called CourseService as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { EventEmitter, Injectable } from '@angular/core';

import { Course } from './course.model';
import { CourseService } from '../course.service';

@Injectable()
export class CourseService {

  private courses: Course[] = [
    new Course('Angular Course 1', 'This is simply a practice Angular course 1', 'http://via.digital.com/350x150.jpeg'),
    new Course('Angular Course 2', 'This is simply a practice Angular course 2', 'http://via.digital.com/350x150'),
	new Course('Angular Course 3', 'This is simply a practice Angular course 3', 'http://via.digital.com/350x150')
  ];

  constructor() {}

  getCourses() {
    return this.courses.slice();
  }
}
javascript

Note above that we are returning a copy of the "courses" using slice() and not the original array so that it cannot be modified outside this service.

Now that we have the "courses" array/model as part of the service, let's remove it from our CourseListComponent. Inside CourseListComponent, we'll get access to the courses using the getCourses() method defined in our CourseService. We just need to define CourseService in our constructor since we want the service to be injected here. So our CourseListComponent would look like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component, OnInit } from '@angular/core';

import { Course } from '../course.model';
import { CourseService } from '../course.service';

@Component({
  selector: 'app-course-list',
  templateUrl: './course-list.component.html',
  styleUrls: ['./course-list.component.css']
})
export class CourseListComponent implements OnInit {
  courses: Course[];

  constructor(private courseService: CourseService) {
  }

  ngOnInit() {
    this.courses = this.courseService.getCourses();
  }
}
javascript

We'll also update the code for event handling to be via services. To do this, we'll add "courseSelected" as a property on the CourseService.

1
2
3
4
5
@Injectable()
export class CourseService {
  courseSelected = new EventEmitter<Course>();

}
javascript

Below is the template for our CourseItemComponent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<a
  href="#"
  class="list-group-item clearfix"
	(click)="onSelected()">
  <div class="pull-left">
    <h4 class="list-group-item-heading">{{ course.name }}</h4>
    <p class="list-group-item-text">{{ course.description }}</p>
  </div>
  <span class="pull-right">
        <img
          [src]="course.courseImagePath"
          alt="{{ course.name }}"
          class="img-responsive"
          style="max-height: 50px;">
      </span>
</a>
html

Inside the onSelected() method, we'll now invoke the emit() method on the "courseSelected" property defined in the service. Here is how our CourseItemComponent would look like (after introducing service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

import { Course } from '../../course.model';
import { CourseService } from '../../course.service';

@Component({
  selector: 'app-course-item',
  templateUrl: './course-item.component.html',
  styleUrls: ['./course-item.component.css']
})
export class CourseItemComponent implements OnInit {
@Input() course: Course;  

  constructor(private courseService: CourseService) { }
  ngOnInit() {  }
onSelected() {
    this.courseService.courseSelected.emit(this.course);
  }
}
javascript

We can now listen/subscribe to this event in our AppComponent. So in our AppComponent, we'll update the ngOnInit() method to subscribe to the above "courseSelected" event and also add "CourseService" to the provider’s array. Below is how the AppComponent would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, OnInit } from '@angular/core';
import { Course } from './course.model';
import { CourseService } from './course.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
	providers: [RecipeService]
})
export class AppComponent implements OnInit {
  selectedCourse: Course;
  constructor(private courseService: CourseService) { }
  ngOnInit() {
	this.courseService.courseSelected
      .subscribe(
        (course: Course) => {
          this.selectedCourse = course;
        }
      );
  }
}
javascript

Conclusion

Thus, we can see that using Angular services is a much better way to pass data around between components instead of manually passing them between various components. Also, our individual components can subscribe to parts of this data store, which greatly reduces the burden on parent/child components in our app to pass data back and forth. Angular services can contain all the business logic which is entirely independent of the components and we can consume them across multiple components. In most practical scenarios, these services are used to make an external API call using REST APIs to get the data and it can act as a bridge between two components i.e. Sender component and Receiver component.

1