如何使用 ControlValueAccessor 在 Angular 中创建自定义表单控件

简介

当在Angular中创建表单时,有时你希望有一个不是标准文本输入、选择或复选框的输入。通过实现ControlValueAccessor接口并将组件注册为NG_VALUE_ACCESSOR,您可以将自定义表单控件无缝集成到模板驱动或响应式表单中,就像它是本机输入一样!

选择不同数量的stars.的评级输入组件示例的动画gif

在本文中,您将把一个基本的星级输入组件转换成一个ControlValueAccessor

前提条件

要完成本教程,您需要:

本教程使用Node v16.4.2、npmv7.18.1、angularv12.1.1进行了验证。

第一步-设置项目

首先创建一个新的RatingInputComponent

这可以通过@angular/cli来实现:

1ng generate component rating-input --inline-template --inline-style --skip-tests --flat --prefix

这会将新组件添加到APP声明中,并生成一个Rating-input.Component.ts文件:

 1[label src/app/rating-input.component.ts]
 2import { Component, OnInit } from '@angular/core';
 3
 4@Component({
 5  selector: 'rating-input',
 6  template: `
 7    <p>
 8      rating-input works!
 9    </p>
10  `,
11  styles: [
12  ]
13})
14export class RatingInputComponent implements OnInit {
15
16  constructor() { }
17
18  ngOnInit(): void {
19  }
20
21}

添加模板、样式和逻辑:

 1[label src/app/rating-input.component.ts]
 2import { Component } from '@angular/core';
 3
 4@Component({
 5  selector: 'rating-input',
 6  template: `
 7    <span
 8      *ngFor="let starred of stars; let i = index"
 9      (click)="rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"
10    >
11      <ng-container *ngIf="starred; else noStar">⭐</ng-container>
12      <ng-template #noStar>·</ng-template>
13    </span>
14  `,
15  styles: [`
16    span {
17      display: inline-block;
18      width: 25px;
19      line-height: 25px;
20      text-align: center;
21      cursor: pointer;
22    }
23  `]
24})
25export class RatingInputComponent {
26  stars: boolean[] = Array(5).fill(false);
27
28  get value(): number {
29    return this.stars.reduce((total, starred) => {
30      return total + (starred ? 1 : 0);
31    }, 0);
32  }
33
34  rate(rating: number) {
35    this.stars = this.stars.map((_, i) => rating > i);
36  }
37}

我们可以通过调用rate函数或点击想要的星数来获取组件的Value(0到5`),并设置组件的值。

您可以将组件添加到应用程序中:

1[label src/app/app.component.html]
2<rating-input></rating-input>

并运行应用程序:

1ng serve

并在网络浏览器中与其交互。

这很好,但我们不能只将此输入添加到表单中,然后期望一切都能正常工作。我们需要将其设置为ControlValueAccessor

第二步-创建自定义表单控件

为了使`RatingInputComponent‘的行为像是本机输入(从而成为真正的定制表单控件),我们需要告诉ANGLE如何做一些事情:

  • 在输入中写入一个值-WriteValue
  • 注册一个函数,在输入值发生变化时通知ANGLE-registerOnChange
  • 注册一个函数,告知ANGLE何时触摸输入-registerOnTouched
  • 关闭输入-setDisabledState

这四个组件组成了ControlValueAccessor接口,它是表单控件和本机元素或自定义输入组件之间的桥梁。一旦我们的组件实现了该接口,我们需要通过将其提供为NG_VALUE_ACCESSOR来告诉Angular它,以便它可以使用。

在代码编辑器中重新访问Rating-input.Component.ts并进行以下更改:

 1[label src/app/rating-input.component.ts]
 2import { Component, forwardRef, HostBinding, Input } from '@angular/core';
 3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 4
 5@Component({
 6  selector: 'rating-input',
 7  template: `
 8    <span
 9      *ngFor="let starred of stars; let i = index"
10      (click)="onTouched(); rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"
11    >
12      <ng-container *ngIf="starred; else noStar">⭐</ng-container>
13      <ng-template #noStar>·</ng-template>
14    </span>
15  `,
16  styles: [`
17    span {
18      display: inline-block;
19      width: 25px;
20      line-height: 25px;
21      text-align: center;
22      cursor: pointer;
23    }
24  `],
25  providers: [
26    {
27      provide: NG_VALUE_ACCESSOR,
28      useExisting: forwardRef(() => RatingInputComponent),
29      multi: true
30    }
31  ]
32})
33export class RatingInputComponent implements ControlValueAccessor {
34  stars: boolean[] = Array(5).fill(false);
35
36  // Allow the input to be disabled, and when it is make it somewhat transparent.
37  @Input() disabled = false;
38  @HostBinding('style.opacity')
39  get opacity() {
40    return this.disabled ? 0.25 : 1;
41  }
42
43  // Function to call when the rating changes.
44  onChange = (rating: number) => {};
45
46  // Function to call when the input is touched (when a star is clicked).
47  onTouched = () => {};
48
49  get value(): number {
50    return this.stars.reduce((total, starred) => {
51      return total + (starred ? 1 : 0);
52    }, 0);
53  }
54
55  rate(rating: number) {
56    if (!this.disabled) {
57      this.writeValue(rating);
58    }
59  }
60
61  // Allows Angular to update the model (rating).
62  // Update the model and changes needed for the view here.
63  writeValue(rating: number): void {
64    this.stars = this.stars.map((_, i) => rating > i);
65    this.onChange(this.value);
66  }
67
68  // Allows Angular to register a function to call when the model (rating) changes.
69  // Save the function as a property to call later here.
70  registerOnChange(fn: (rating: number) => void): void {
71    this.onChange = fn;
72  }
73
74  // Allows Angular to register a function to call when the input has been touched.
75  // Save the function as a property to call later here.
76  registerOnTouched(fn: () => void): void {
77    this.onTouched = fn;
78  }
79
80  // Allows Angular to disable the input.
81  setDisabledState(isDisabled: boolean): void {
82    this.disabled = isDisabled;
83  }
84}

这段代码将允许禁用输入,当它被禁用时,它将变得有些透明。

运行应用程序:

1ng serve

并在网络浏览器中与其交互。

您还可以禁用输入控件:

1[label src/app/app.component.html]
2<rating-input [disabled]="true"></rating-input>

我们现在可以说我们的RatingInputComponent是一个自定义表单组件!它将像任何其他本机输入一样工作(ANGLE为这些输入提供了ControlValueAccessors!)以模板驱动或反应性形式。

结论

在本文中,您将一个基本的星级输入组件转换为一个ControlValueAccessor

现在你会注意到:

如果你想了解更多关于Angular的知识,请查看我们的Angular主题页面获取练习和编程项目。

Published At
Categories with 技术
Tagged with
comments powered by Disqus