65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年11月20日

CPOL

10分钟阅读

viewsIcon

15535

downloadIcon

129

使用 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

在其中添加 RollNoNameMarks 属性。

export class Student {
  RollNo: number;
  Name: string;
  Marks: number;
}

这里,export 意味着这个 classpublic 的。

现在,通过添加一个名为 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 服务、本地存储或模拟数据源。

将数据访问从组件中移除意味着你可以在任何时候改变实现方式,而无需触及任何组件。它们不知道服务是如何工作的。

现在导入 StudentStudents 如下:

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

此外,为了使数据从服务异步传输,从 rxjs 导入 observableof,如下所示:

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'

现在通过向构造函数添加类型为 StudentrecordServiceprivate 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 并导入 InputStudent。声明一个类型为 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 { }
© . All rights reserved.