Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Component Interaction In Angular (How Input Output Works)

4.50/5 (4 votes)
25 Nov 2018CPOL10 min read 14.8K   91  
Component Interaction in Angular using Input Output decorator

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.

JavaScript
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.

JavaScript
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:

JavaScript
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:

JavaScript
import { Student } from './student'
import { Students } from './StudentMockData'

Also, to make data transfer asynchronous from service import observable and of from rxjs like below:

JavaScript
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:

JavaScript
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:

JavaScript
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.

JavaScript
import { StudentrecordService } from './studentrecord.service'

Now inject the StudentrecordService by adding a private studentRecordService parameter of type StudentrecordService to the constructor.

JavaScript
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.

JavaScript
import { Student } from './student'

students: Student[];

Now add a getStudentRecord method that will call the studentRecordService to get the student record.

JavaScript
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:

JavaScript
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:

JavaScript
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.

JavaScript
ng g c StudentDetail

Open StudentDetailComponent and import Input and Student. Declare a studentsData variable of type Student array like below:

JavaScript
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.

HTML
<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.

CSS
[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.

JavaScript
 @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:

JavaScript
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 students record.

HTML
<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 students 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 students, 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.

JavaScript
@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:

JavaScript
<h4><input [(ngModel)]="student.Marks"  (change)="valueChnaged(student)" placeholder="Marks"></h4>

Now add that valueChanged() method into the component file, i.e., StudentDetailComponent.

JavaScript
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:

JavaScript
<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.

JavaScript
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:

JavaScript
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:

HTML
<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:

JavaScript
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:

JavaScript
<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:

JavaScript
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 { }

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)