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

来自菜鸟教程
跳转至:导航、​搜索

介绍

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

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

先决条件

要完成本教程,您需要:

本教程已使用 Node v16.4.2、npm v7.18.1、angular v12.1.1 进行了验证。

第 1 步 — 设置项目

首先,新建一个RatingInputComponent

这可以用 @angular/cli 来完成:

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

这会将新组件添加到应用程序 declarations 并生成一个 rating-input.component.ts 文件:

src/app/rating-input.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'rating-input',
  template: `
    <p>
      rating-input works!
    </p>
  `,
  styles: [
  ]
})
export class RatingInputComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

添加模板、样式和逻辑:

src/app/rating-input.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'rating-input',
  template: `
    <span
      <^>*ngFor="let starred of stars; let i = index"
      (click)="rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"<^>
    >
      <ng-container *ngIf="starred; else noStar">⭐</ng-container>
      <ng-template #noStar>·</ng-template>
    </span>
  `,
  styles: [`
    span {
      display: inline-block;
      width: 25px;
      line-height: 25px;
      text-align: center;
      cursor: pointer;
    }
  `]
})
export class RatingInputComponent {
  stars: boolean[] = Array(5).fill(false);

  get value(): number {
    return this.stars.reduce((total, starred) => {
      return total + (starred ? 1 : 0);
    }, 0);
  }

  rate(rating: number) {
    this.stars = this.stars.map((_, i) => rating > i);
  }
}

我们可以通过调用rate函数或者点击数量明星想要。

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

src/app/app.component.html

<rating-input></rating-input>

并运行应用程序:

ng serve

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

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

第 2 步 — 创建自定义表单控件

为了使 RatingInputComponent 表现得好像它是一个原生输入(因此,一个真正的自定义表单控件),我们需要告诉 Angular 如何做一些事情:

  • 将值写入输入 - writeValue
  • 注册一个函数以在输入值更改时告诉 Angular - registerOnChange
  • 注册一个函数以在输入被触摸时告诉 Angular - registerOnTouched
  • 禁用输入 - setDisabledState

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

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

src/app/rating-input.component.ts

import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'rating-input',
  template: `
    <span
      *ngFor="let starred of stars; let i = index"
      (click)="onTouched(); rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"
    >
      <ng-container *ngIf="starred; else noStar">⭐</ng-container>
      <ng-template #noStar>·</ng-template>
    </span>
  `,
  styles: [`
    span {
      display: inline-block;
      width: 25px;
      line-height: 25px;
      text-align: center;
      cursor: pointer;
    }
  `],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingInputComponent),
      multi: true
    }
  ]
})
export class RatingInputComponent implements ControlValueAccessor {
  stars: boolean[] = Array(5).fill(false);

  // Allow the input to be disabled, and when it is make it somewhat transparent.
  @Input() disabled = false;
  @HostBinding('style.opacity')
  get opacity() {
    return this.disabled ? 0.25 : 1;
  }

  // Function to call when the rating changes.
  onChange = (rating: number) => {};

  // Function to call when the input is touched (when a star is clicked).
  onTouched = () => {};

  get value(): number {
    return this.stars.reduce((total, starred) => {
      return total + (starred ? 1 : 0);
    }, 0);
  }

  rate(rating: number) {
    if (!this.disabled) {
      this.writeValue(rating);
    }
  }

  // Allows Angular to update the model (rating).
  // Update the model and changes needed for the view here.
  writeValue(rating: number): void {
    this.stars = this.stars.map((_, i) => rating > i);
    this.onChange(this.value);
  }

  // Allows Angular to register a function to call when the model (rating) changes.
  // Save the function as a property to call later here.
  registerOnChange(fn: (rating: number) => void): void {
    this.onChange = fn;
  }

  // Allows Angular to register a function to call when the input has been touched.
  // Save the function as a property to call later here.
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // Allows Angular to disable the input.
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

此代码将允许禁用输入,并在启用时使其有点透明。

运行应用程序:

ng serve

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

您还可以禁用输入控件:

src/app/app.component.html

<rating-input [disabled]="true"></rating-input>

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

结论

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

你现在会注意到:

如果您想了解有关 Angular 的更多信息,请查看 我们的 Angular 主题页面 以获取练习和编程项目。