组件交互在 Angular 中(Input Output 如何工作)





5.00/5 (1投票)
使用 Input Output 装饰器在 Angular 中进行组件交互
引言
本文旨在介绍 Angular 应用中的组件交互。我们将通过一个小型“尖子生名单”应用来学习数据如何在组件之间传递,同时,我们还将学习如何用不同的数据集实例化一个组件。
必备组件
阅读本文前,你应该了解以下技术和工具:
- Typescript
- Angular 基础知识
- Angular CLI
- 节点
- NPM
Using the Code
我们知道,Angular 应用程序是基于小型组件构建的,因此将数据从父
组件传递到子组件有些棘手。在这种情况下,@Input
和 @Output
装饰器就派上用场了。Angular 组件有更好的方式通过事件通知父组件发生了变化。Input 指定了你可以从父组件设置的组件属性,而“Output”则标识了组件可以触发的事件,用于将信息从子组件发送到其父组件。
在本教程中,我们将根据学生的成绩展示三组学生:尖子生
、中等生
和普通生
。同时,用户可以编辑分数,并且该特定编辑学生的记录将根据编辑后的分数被移到尖子生
、中等生
或普通生
的特定部分。
让我们通过代码来看一下。
注意:我们正在使用 Angular CLI 生成项目和组件文件。
在命令提示符中,输入以下命令来创建一个项目:
ng new TopperStudentsList
此命令将在你的命令提示符打开的指定位置生成一个项目。
生成的项目将配置所有基本设置,并包含一个带有根模块和组件的 app 文件夹。
为了在组件之间共享数据,我们至少需要两个组件。我们将生成一个组件,而我们要使用的第二个组件是 app 组件。它是一个根组件。
注意:像我们这样使用 app 组件进行项目数据操作并不是一个好习惯。app 组件用于提供系统入口。但由于这是一个演示项目,我们也将使用此组件进行其他用途。
所以我们的第一个组件是 app 组件,我们的第二个组件将是 student detail 组件。进入命令提示符并输入以下命令以生成一个新组件。
ng g c student-detail
这里的 g
代表 generate,这里的 c
代表 component。
执行此命令后,它将创建一个单独的文件夹,其中包含四个文件。一个是它的组件文件,一个是它的 HTML 文件,一个是它的 CSS 文件,一个是它的测试文件。
现在,创建一个 Student
类。为此,右键单击 app 文件夹并选择新建文件。将其命名为 Student.ts。
在其中添加 RollNo
、Name
和 Marks
属性。
export class Student {
RollNo: number;
Name: string;
Marks: number;
}
这里,export
意味着这个 class
是 public
的。
现在,通过添加一个名为 StudentMockData.ts 的单独文件来创建模拟学生
记录,该文件将包含学生
详细信息。为此,右键单击 app 文件夹并选择新建文件。将其命名为 StudentMockData.ts。
创建一个常量 Students
变量,它是我们最近创建的 Student
类数组类型,并以 json 数组的形式填充模拟学生
数据。
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
}
];
现在,我们已经准备好 Student Class
及其数据。所以接下来的工作是将学生
数据填充到学生
类中并在屏幕上呈现它。
我们也可以在组件类中完成这项工作,但组件不应直接获取或保存数据。它们只应负责呈现数据并将数据访问委托给服务。
所以我们将创建一个 StudentRecordService
,所有应用程序类都可以使用它来获取 studentrecord
。要生成服务,请使用以下命令:
ng g s StudentRecord
它将创建一个名为 StudentRecordService
的服务类,文件名将是 studentRecord.service.ts。在生成服务时,你不需要在文件名中写入 service,因为这是 cli
的任务。
Cli
将生成一个如下所示的模板:
import { Injectable } from '@angular/core';
@Injectable()
export class StudentRecordService {
constructor() { }
}
请注意,新服务导入了 Angular 的 Injectable 符号,并使用 @Injectable()
装饰器注释了该类。这标志着该类参与了依赖注入系统。StudentrecordService
类将提供一个可注入的服务,并且它也可以拥有自己的注入依赖项。
StudentRecordService
可以从任何地方获取英雄数据——Web 服务、本地存储或模拟数据源。
将数据访问从组件中移除意味着你可以在任何时候改变实现方式,而无需触及任何组件。它们不知道服务是如何工作的。
现在导入 Student
和 Students
如下:
import { Student } from './student'
import { Students } from './StudentMockData'
此外,为了使数据从服务异步传输,从 rxjs 导入 observable
和 of
,如下所示:
import { Observable } from 'rxjs'
import { of } from 'rxjs/observable/of'
现在,创建一个名为 getStudentRecord()
的方法,其类型为 Observable
,它将向调用组件返回 Students
数据。服务的实际代码如下:
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);
}
}
这里 of(Students)
返回一个 Observable
,它发出一个单一值,即模拟英雄数组。
我们的服务已经准备好了。现在我们只需要将 StudentrecordService
导入到 app 模块中,并像下面这样在 NgModule
的 providers 中注册它:
import { StudentrecordService } from './studentrecord.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [StudentrecordService],
bootstrap: [AppComponent]
})
export class AppModule { }
由于我们必须在 app 组件中使用此服务,因此在 AppComponent
类中导入 StudentrecordService
。
import { StudentrecordService } from './studentrecord.service'
现在通过向构造函数添加类型为 StudentrecordService
的 private studentRecordService
参数来注入 StudentrecordService
。
constructor(private studentRecordService: StudentrecordService) { }
该参数同时定义了一个私有 studentRecordService
属性,并将其标识为一个 StudentrecordService
注入点。
当 Angular 创建一个 AppComponent
时,依赖注入系统将 studentRecordService
参数设置为 AppComponent
的单例实例。
导入 Student
并在 AppComponent class
内部声明一个类型为 Student
数组的变量 students
,其中可以填充来自 studentRecordService
的数据。
import { Student } from './student'
students: Student[];
现在添加一个 getStudentRecord
方法,该方法将调用 studentRecordService
以获取学生记录。
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe(studentdata => this.students = studentdata);
}
由于 StudentRecordService
返回 Observable
,我们必须像上面那样订阅它。它将等待 Observable
发出学生
数组,这可能现在发生,也可能在几分钟后发生。然后 subscribe
将发出的数组传递给回调函数,该回调函数设置组件的 students
属性。
现在我们方案的主要要求是根据分数在尖子生、中等生和普通生三个部分显示学生
记录。为此,我们需要以排序方式获取学生
记录。我们将添加一个函数,根据分数对学生
记录进行排序,如下所示:
getStudentRecord(): void {
this.studentRecordService.getStudentRecord().subscribe
(studentdata => this.students = studentdata.sort(function (a, b) {
return a.Marks - b.Marks;
}).reverse());
}
这将按从高到低(即降序)的方式返回学生
记录。
现在在 ngOnInit
生命周期钩子内部调用 getStudentRecord
,让 Angular 在构造 AppComponent
实例后在适当的时间调用 ngOnInit
。为此,你需要为 AppComponent
类实现 onInit
。在 onInit
内部调用示例如下:
ngOnInit() {
this.getStudentRecord();
}
现在为了显示学生
记录,我们将创建另一个组件。通过这种方式,我们也将学习不同组件之间如何交互。在命令提示符中输入以下命令以生成新的 StudentDetailComponent
。
ng g c StudentDetail
打开 StudentDetailComponent
并导入 Input
和 Student
。声明一个类型为 Student
数组的 studentsData
变量,如下所示:
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[];
}
现在打开它的 HTML 文件,即当我们执行生成 StudentDetailComponent
的命令时 CLI 生成的 student-detail-component.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>
由于我们要显示每个学生
记录,我们将遍历组件类中声明的数组类型 StudentData
对象。
注意:对于分数,我们已将字段设置为输入,并且它是双向绑定的,因为用户也可以编辑分数,并且根据编辑后的分数,用户记录将相应地显示在尖子生
部分、中等生
部分或其余部分。
注意:下面我列出了我使用过的 CSS 类。将这些类粘贴到此组件的 .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;
}
}
现在,这里出现的问题是,这个 studentsData
将如何用学生
记录数据填充。作为回应,@Input
装饰器就派上用场了。我们将用 @Input
装饰器标记 studentsData
,我们知道如果变量用 @Input
装饰器标记,那么它将能够接收来自另一个组件的数据。在我们的例子中,它将接收来自 AppComponent
的数据。让我们看看这是如何发生的。
首先,在 studentsData
中放置一个 @Input
标签。
@Input() studentsData: Student[];
让我们再次打开 AppComponent
。如前所述,我们将在尖子生
、中等生
和普通生
三个部分显示学生
记录,因此我们将在 AppComponent
类中创建三个函数:getToppersRecord()
、getMediocoreRecord()
和 getLowerRecord()
,分别用于获取尖子生
、中等生
和其余的学生,如下所示:
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();
}
}
由于这是一个演示项目,我们创建了 15 条常量记录,并显示前 5 名、中等 5 名和其余 5 名,所以我们使用了参数为 5 的 slice 函数。此外,这三个方法只对 student
变量起作用,该变量仅在创建 AppComponent
实例时才填充。
现在,打开 app.component.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>
由于我们已经创建了一个单独的 StudentDetail
组件来显示学生
记录,所以我们在这里使用该组件来显示记录。
现在你可以看到,我们通过调用三个不同的方法实例化了一个组件,并使用了三种不同的数据,即对于尖子生
,我们调用了 getToppersRecord()
方法,该方法将带来前 5 名学生
的数据,getMediocoreRecord()
和 getLowerRecord()
也类似,分别获取中等生和差生的记录。
你必须注意到的一件事是,函数调用返回的数据被赋值给变量 studentsData
。这与我们在 StudentDetailComponent
中声明的带有 @Input
标签的变量相同。此外,对于 Input
变量,我们使用方括号。
因此,我们在 StudentDetailComponent
HTML 文件中列出学生
记录时提出的问题是,我们循环获取每个学生
记录的 studentData
将如何填充,答案就在这里。
所以代码流程是 getToppersRecord()
会填充 studentsData
,这些数据将在 StudentDetailComponent
内部可访问,并通过它的 HTML 文件在屏幕上显示。
如前所述,我们也可以编辑分数,并相应地放置学生
记录。换句话说,这种情况将被视为将数据从子组件传递到父组件。将数据从子组件传递到父组件有点棘手。在这种情况下,子组件没有对父组件的任何引用。因此,在这种情况下,我们需要从子组件发出一个事件,父组件将监听它并通过事件接收数据并显示它。为了实现此功能,Angular 的 @Output
标签将发挥作用。现在让我们来处理此功能。
打开 student
-detail
-component
文件并使用 @Output()
标签声明变量 MarksValueChnaged
。用 EventEmitter
对象初始化它。
@Output() MarksValueChnaged = new EventEmitter();
现在打开 student-detail-component.html 并添加 change 事件,在该事件上,调用 valueChnaged
方法到 marks 字段,如下所示:
<h4><input [(ngModel)]="student.Marks" (change)="valueChnaged(student)" placeholder="Marks"></h4>
现在将 valueChanged()
方法添加到组件文件,即 StudentDetailComponent
中。
valueChnaged(newData: Student) {
this.MarksValueChnaged.emit(newData);
}
正如你在 HTML 文件中看到的,当在 change
事件上调用 valueChange
方法时,它正在传递具有更新分数的新数据的学生
对象。因此,在 valueChnaged
方法中,我们将把这些新数据发出给其父组件。
在 app.component.html 文件中使用事件绑定,并监听事件发射器,如下所示:
<app-student-detail [studentsData]='getToppersRecord()'(MarksValuealueChnaged)=
"trapValueChanged($event)"></app-student-detail>
我们需要在 app.component.ts 文件中定义 trapValueChanged
方法。此方法将用于使用新分数更新学生
记录,并相应地更新其在三个部分之一中的位置。
trapValueChanged(value: Student) {
this.students.find(x => x.RollNo == value.RollNo).Marks = value.Marks;
this.getStudentRecord();
}
我们的解决方案到此结束。我们在这里学习了如何使用 Input、Output 装饰器在组件之间共享数据,以及如何使用不同的数据集实例化一个组件。
app.component.ts 的最终代码将如下所示:
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();
}
}
最终的 app.component.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>
最终的 student-detail.component.ts 将如下所示:
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);
}
}
最终的 student-detail.component.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"
(change)="valueChnaged(student)" placeholder="Marks"></h4>
</div>
</a>
</div>
</body>
最终的 app.module.ts 如下所示:
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 { }