Introduction
This article is all about component interaction in Angular application. we will learn how data is passed from one component to another component through a small "Topper Student List" application and also, we will learn how we can instantiate one component with a different data set.
Prerequisites
For this article, you should be aware of the below mentioned technologies and tools:
- Typescript
- Basics of Angular
- Angular Cli
- Node
- NPM
Using the Code
As we know, Angular Application is built upon small components so passing the data from Parent
component to child component is a little bit tricky, In that scenario, @Input
and @Output
decorator come in handy. Angular components have a better way of notifying parent components that something has changed via events. Inputs specify which properties you can set on a component from a parent whereas “Outputs” identify the events a component can fire to send information up the hierarchy to its parent from its child component.
In this tutorial, we will display 3 groups of students, Topper
, Mediocre
and Average
ones based on their marks and at the same time, user can edit the marks and the record of that particular edited student will be shifted to a particular section of topper
, mediocre
or average
student based on the marks edited.
Let's see it through code.
Note: We are using Angular CLI to generate the project and component files.
In command prompt, type the following command to make a project:
ng new TopperStudentsList
This command will generate a project on a specified location where your command prompt is open.
The generated project will have all the basic settings configured and an app folder with root module and component.
To share the data between components, we need at least two components. We will generate one component and a second component that we will use is app component. It is a root component.
Note: It's not a good practice to use app component for the project data manipulation as we are going to use it. app component is used to provide entry to your system. but as it's a demo project, we are going to use this component for our other uses also.
So our first component is app component and our second component will be student detail component. Go to command prompt and type the following command to generate a new component.
ng g c student-detail
g
here stands for generate and c
here stands for component.
After hitting this command, it will create a separate folder with four files. One is its component file, one is its HTML file, one is its CSS file and one is its test file.
Now, create a Student
class. For that, right click on the app folder and choose new file. Name it Student.ts.
Add RollNo
, Name
and Marks
property in it.
export class Student {
RollNo: number;
Name: string;
Marks: number;
}
Here, export
means this class
is public
.
Now, create dummy student
records by adding a separate file with the name of StudentMockData.ts that will contain student
details. For that, right click on the app folder and choose new file. Name it StudentMockData.ts.
Create a constant Students
variable which is a type of array of Student
class that we have recently created and fill the dummy student
data in a form of json array.
import { Student } from './student'
export const Students: Student[] = [
{
RollNo: 1,
Name: "Roshan",
Marks: 78
},
{
RollNo: 2,
Name: "Rahul",
Marks: 43
},
{
RollNo: 3,
Name: "Gaurav",
Marks: 85
},
{
RollNo: 4,
Name: "Mohit",
Marks: 80
},
{
RollNo: 5,
Name: "sohit",
Marks: 90
},
{
RollNo: 6,
Name: "sohan",
Marks: 89
},
{
RollNo: 7,
Name: "Lalit",
Marks: 67
},
{
RollNo: 8,
Name: "Raghav",
Marks: 75
},
{
RollNo: 9,
Name: "vikas",
Marks: 83
},
{
RollNo: 10,
Name: "veer",
Marks: 57
},
{
RollNo: 11,
Name: "sameer",
Marks: 67
},
{
RollNo: 12,
Name: "sourabh",
Marks: 89
},
{
RollNo: 13,
Name: "Gauri",
Marks: 76
},
{
RollNo: 14,
Name: "Roshan",
Marks: 56
},
{
RollNo: 15,
Name: "Hemant",
Marks: 76
}
];
Now, we are ready with Student Class
and its data. So the next job is to fill the student
data in the student
class and to present it on screen.
We can do this job in a component class also but component should not fetch or save data directly. They should only be responsible for presenting data and delegate data access to service.
So we will create a StudentRecordService
that all application classes can use to get studentrecord
. To generate service, use the below command:
ng g s StudentRecord
It will create a service class with name StudentRecordService
and file name will be studentRecord.service.ts. You don't need to write service while giving file name while generating service as it's a task of cli
to add it.
Cli
will generate a template like below:
import { Injectable } from '@angular/core';
@Injectable()
export class StudentRecordService {
constructor() { }
}
Notice that the new service imports the Angular Injectable symbol and annotates the class with the @Injectable()
decorator. This marks the class as one that participates in the dependency injection system. The StudentrecordService
class is going to provide an injectable service, and it can also have its own injected dependencies.
The StudentRecordService
could get hero data from anywhere—a web service, local storage, or a mock data source.
Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works.
Now import Student
and Students
like below:
import { Student } from './student'
import { Students } from './StudentMockData'
Also, to make data transfer asynchronous from service import observable
and of
from rxjs like below:
import { Observable } from 'rxjs'
import { of } from 'rxjs/observable/of'
Now, create a method with name getStudentRecord()
which is of type Observable<Student[]>
that will return the Students
data to the calling component. The actual code of the service will be like below:
import { Injectable } from '@angular/core';
import { Student } from './student'
import { Students } from './StudentMockData'
import { Observable } from 'rxjs';
import { of } from 'rxjs/observable/of';
@Injectable()
export class StudentrecordService {
getStudentRecord(): Observable<Student[]> {
return of(Students);
}
}
Here of(Students)
returns an Observable<Student[]>
that emits a single value, the array of mock heroes.
Our service is ready. Now we just have to import StudentrecordService
into the app module and register it in providers of NgModule
like below:
import { StudentrecordService } from './studentrecord.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [StudentrecordService],
bootstrap: [AppComponent]
})
export class AppModule { }
As we have to use this service in app component, so import StudentrecordService
in AppComponent
class.
import { StudentrecordService } from './studentrecord.service'
Now inject the StudentrecordService
by adding a private studentRecordService
parameter of type StudentrecordService
to the constructor.
constructor(private studentRecordService: StudentrecordService) { }
The parameter simultaneously defines a private studentRecordService
property and identifies it as a StudentrecordService
injection site.
When Angular creates an AppComponent
, the Dependency Injection system sets the studentRecordService
parameter to the singleton instance of AppComponent
.
Import Student
and declare a variable students
of type Student
array inside AppComponent class
in which data from the studentRecordService
can be filled.
import { Student } from './student'
students: Student[];
Now add a getStudentRecord
method that will call the studentRecordService
to get the student record.
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe(studentdata => this.students = studentdata);
}
As StudentRecordService
returns Observable<Student[]>
, we have to subscribe it as we have done above. It will wait for the Observable
to emit the array of student
which could happen now or several minutes from now. Then subscribe
passes the emitted array to the callback, which sets the component's students
property.
Now our main requirement of this solution is to show student
record in three sections, that is of topper
, mediocre
and of average
students on the basis of marks. For that, we need student
records in a sorted manner. We will add a function to sort the student
record on the basis of marks as below:
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe
(studentdata => this.students = studentdata.sort(function (a, b) {
return a.Marks - b.Marks;
}).reverse());
}
This will return the student
record in a sorted manner from topper to lower, i.e., in descending manner.
Now call getStudentRecord
inside the ngOnit
lifecycle hook and let Angular call ngOnInit
at an appropriate time after constructing an AppComponent
instance. For this, you need to implement onInit
to the class AppComponent
. Calling inside the onInit
is shown below:
ngOnInit() {
this.getStudentRecord();
}
Now for displaying student
record, we will make another component. In this way, we will learn also how different components interact with each other. Enter the below command in command prompt to generate new StudentDetailComponent
.
ng g c StudentDetail
Open StudentDetailComponent
and import Input
and Student
. Declare a studentsData
variable of type Student
array like below:
import { Component,Input } from '@angular/core';
import { Student } from '../student'
@Component({
selector: 'app-student-detail',
templateUrl: './student-detail.component.html',
styleUrls: ['./student-detail.component.css']
})
export class StudentDetailComponent {
studentsData: Student[];
}
Now open its HTML file that is student-detail-component.html that is generated by CLI when we hit that command to generate StudentDetailComponent
. We will add here the below written code to display student
record.
<body>
<div class="grid raisedbox">
<a *ngFor="let student of studentsData" class="col-1-4">
<div class="module">
<h4 class="badge">RollNo : {{student.RollNo}}</h4>
<h4 class="badge">Name : {{student.Name}}</h4>
<h4><input [(ngModel)]="student.Marks" placeholder="Marks"></h4>
</div>
</a>
</div>
</body>
As we have to show each student
record, we will iterate through StudentData
object that is of array type declared in component
class.
Note: For marks, we have taken the field as of input and it's having a two way binding because user can edit the marks also and on that basis of edited marks, user record will be shown accordingly in toppers
section or in mediocre
section or the rest in other.
Note: Below, I am listing the CSS classes that I have used. Paste these classes in the .css file of this component or else, you can style it in your own way.
[class*='col-'] {
float: inherit;
padding-right: 2px;
padding-bottom: 5px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.raisedbox {
padding: 10px;
border: 1px solid #2f3133;
box-shadow: -1px 1px #2f3133, -2px 2px #2f3133, -3px 3px #2f3133, -4px 4px #2f3133, -5px 5px #2f3133;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 1%;
}
.module {
padding: 5px;
text-align: left;
color: #eee;
background-color: #2f3133;
border-radius: 2px;
}
.module:hover {
background-color: #eee;
cursor: pointer;
color: #070707;
}
body {
background-color: #d0deed;
}
.module {
min-width: 60px;
}
}
Now, the question that comes up here is, how this studentsData
is going to be filled with the student
record data. So in response to it, here comes @Input
decorator in the picture. We will mark studentsData
with the @Input
decorator and we know if the variable is marked with @Input
decorator, then it will be able to receive data from another component. And in our case, it will receive data from AppComponent
. Let's see how this is going to happen.
First of all, place a @Input
tag with studentsData
.
@Input() studentsData: Student[];
Let's open the AppComponent
again. As mentioned earlier, we will display student
record in three sections of topper
, mediocre
and average
so we will create three functions, getToppersRecord()
, getMediocoreRecord()
and getLowerRecord()
inside the AppComponent
class for getting Toppers
, mediocre
and rest of the students like below:
export class AppComponent implements OnInit {
constructor(private studentRecordService: StudentrecordService) { }
students: Student[];
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe
(studentdata => this.students = studentdata.sort(function (a, b) {
return a.Marks - b.Marks;
}).reverse());
}
getToppersRecord() {
return this.students.slice(0, 5);
}
getMediocoreRecord() {
return this.students.slice(5, 10);
}
getLowerRecord() {
return this.students.slice(10);
}
ngOnInit() {
this.getStudentRecord();
}
}
As it's a dummy project, we have created constant 15 records and we are displaying top 5, mediocre 5 and rest 5 others, so we have used slice function with the parameter of 5. Also, these three methods are playing only on student
variable that is filled only when the instance of AppComponent
is created.
Now, open app.component.html and add the below code to get three different category student
s record.
<div class="wrapper">
<div class="column" >
<h2>Toppers</h2>
<app-student-detail [studentsData]='getToppersRecord()'></app-student-detail>
</div>
<div class="column">
<h2>Avarage</h2>
<app-student-detail [studentsData]='getMediocoreRecord()></app-student-detail>
</div>
<div class="column">
<h2>Rest Others</h2>
<app-student-detail [studentsData]='getLowerRecord()'></app-student-detail>
</div>
</div>
As for displaying student
s records, we have created a separate component StudentDetail
so we have used that component here for displaying the records.
Now you can see that we have instantiated a single component with 3 different data by calling three different methods, i.e., for Toppers
, we have called getToppersRecord()
method that will bring data of top 5 student
s, same goes for getMediocoreRecord()
and getLowerRecord()
to get mediocre and lower students records respectively.
One thing you must have noticed here is that the data returned by the function calling is assigned into the variable studentsData
. It is the same variable that we have declared in the StudentDetailComponent
with a @Input
tag. Also, for Input
variable, we use square brackets.
So the question we have raised while listing student
record in StudentDetailComponent
HTML file is how this studentData
is going to be filled on which we are looping to get each student
record, for that, the answer is this.
So the code flow goes like getToppersRecord()
will fill the studentsData
and that data will be accessible inside the StudentDetailComponent
and through there, it will be get displayed through its HTML file on screen.
As we have mentioned earlier, we can edit the marks also and accordingly, the student
record will get placed. In other words, this scenario will be considered as passing data from child component to parent component. Passing the data from child component to parent component is a little bit tricky. In this scenario, child component does not have any reference to the parent component. So in this case, we need to emit an event from child component, and parent component will listen to it and receive the data via event and display it. To achieve this functionality, the @Output
tag of Angular will come into the picture. Let's work on this functionality now.
Open student
-detail
-component
file and declare variable MarksValueChnaged
with @Output()
tag. Initialize it with EventEmitter
object.
@Output() MarksValueChnaged = new EventEmitter();
Now open student-detail-component.html and add change event and on that event, call a method valueChnaged
to the marks field like below:
<h4><input [(ngModel)]="student.Marks" (change)="valueChnaged(student)" placeholder="Marks"></h4>
Now add that valueChanged()
method into the component file, i.e., StudentDetailComponent
.
valueChnaged(newData: Student) {
this.MarksValueChnaged.emit(newData);
}
As you can see in the HTML file, when valueChange
method is called on change
event, it is passing the student
object with new data having updated marks. So in valueChnaged
method, we will emit this new data to its parent component.
Use an event binding in the app.component.html file and listen for the event emitter like below:
<app-student-detail [studentsData]='getToppersRecord()'(MarksValuealueChnaged)=
"trapValueChanged($event)"></app-student-detail>
We need to define trapValueChanged
method into the app.component.ts file. This method will be used to update the student
record with new marks and accordingly its position in one of the three sections.
trapValueChanged(value: Student) {
this.students.find(x => x.RollNo == value.RollNo).Marks = value.Marks;
this.getStudentRecord();
}
Our solution ends here. We have learnt here about data sharing between components using Input, Output decorators and instantiating one component with different data set.
The final code of the app.component.ts will be as shown below:
import { Component, OnInit } from '@angular/core';
import { StudentrecordService } from './studentrecord.service'
import { Student } from './student'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'app';
constructor(private studentRecordService: StudentrecordService) { }
students: Student[];
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe
(studentdata => this.students = studentdata.sort(function (a, b) {
return a.Marks - b.Marks;
}).reverse());
}
getToppersRecord() {
return this.students.slice(0, 5);
}
trapValueChanged(value: Student) {
this.students.find(x => x.RollNo == value.RollNo).Marks = value.Marks;
this.getStudentRecord();
}
getMediocoreRecord() {
return this.students.slice(5, 10);
}
getLowerRecord() {
return this.students.slice(10);
}
ngOnInit() {
this.getStudentRecord();
}
}
The final app.component.html will look like below:
<div class="wrapper">
<div class="column" >
<h2>Toppers</h2>
<app-student-detail [studentsData]='getToppersRecord()'(MarksValuealueChnaged)=
"trapValueChanged($event)"></app-student-detail>
</div>
<div class="column">
<h2>Avarage</h2>
<app-student-detail [studentsData]='getMediocoreRecord()'(MarksValuealueChnaged)=
"trapValueChanged($event)"></app-student-detail>
</div>
<div class="column">
<h2>Rest Others</h2>
<app-student-detail [studentsData]='getLowerRecord()' (MarksValuealueChnaged)=
"trapValueChanged($event)"></app-student-detail>
</div>
</div>
The final student-detail.component.ts will look like below:
import { Component, OnInit, Input , Output, EventEmitter } from '@angular/core';
import { Student } from '../student'
@Component({
selector: 'app-student-detail',
templateUrl: './student-detail.component.html',
styleUrls: ['./student-detail.component.css']
})
export class StudentDetailComponent {
@Input() studentsData: Student[];
@Output() MarksValuealueChnaged = new EventEmitter();
counter = 0;
valueChnaged(newData: Student) {
this.MarksValuealueChnaged.emit(newData);
}
}
The final student-detail.component.html looks like below:
<body>
<div class="grid raisedbox">
<a *ngFor="let student of studentsData" class="col-1-4">
<div class="module">
<h4 class="badge">RollNo : {{student.RollNo}}</h4>
<h4 class="badge">Name : {{student.Name}}</h4>
<h4><input [(ngModel)]="student.Marks"
(change)="valueChnaged(student)" placeholder="Marks"></h4>
</div>
</a>
</div>
</body>
The final app.module.ts looks like below:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { OrderModule } from 'ngx-order-pipe';
import { AppComponent } from './app.component';
import { StudentDetailComponent } from './student-detail/student-detail.component';
import { StudentrecordService } from './studentrecord.service';
@NgModule({
declarations: [
AppComponent,
StudentDetailComponent
],
imports: [
BrowserModule,
FormsModule,
OrderModule
],
providers: [StudentrecordService],
bootstrap: [AppComponent]
})
export class AppModule { }