简介
在 Angular 中创建表单时,有时您希望拥有一个不是标准文本输入、选择或复选框的输入。通过实现 ControlValueAccessor
接口并将组件注册为 NG_VALUE_ACCESSOR
,您可以将自定义表单控件无缝地集成到模板驱动或响应式表单中,就像它是一个原生输入一样!
!Rating Input 组件示例的动画 GIF,选择不同数量的星星。
在本文中,您将把一个基本的星级评分输入组件转换为 ControlValueAccessor
。
先决条件
要完成本教程,您需要:
- 本地安装 Node.js,您可以按照《如何安装 Node.js 并创建本地开发环境》进行操作。
- 一些设置 Angular 项目和使用 Angular 组件的基础知识可能会有所帮助。
本教程已在 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
文件:
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 { } }
添加模板、样式和逻辑:
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); } }
我们可以获取组件的 value
(从 0
到 5
),并通过调用 rate
函数或点击所需的星级来设置组件的值。
您可以将组件添加到应用程序中:
<rating-input></rating-input>
然后运行应用程序:
ng serve
并在 Web 浏览器中进行交互。
这很棒,但我们不能只是将此输入添加到表单中并期望一切立即正常工作。我们需要将其设置为 ControlValueAccessor
。
步骤 2 — 创建自定义表单控件
为了使 RatingInputComponent
表现得就像是一个原生输入(因此是一个真正的自定义表单控件),我们需要告诉 Angular 如何做一些事情:
- 写入输入的值 -
writeValue
- 注册一个函数,告诉 Angular 输入的值何时发生变化 -
registerOnChange
- 注册一个函数,告诉 Angular 输入已被触摸 -
registerOnTouched
- 禁用输入 -
setDisabledState
这四个内容构成了 ControlValueAccessor
接口,它是表单控件和原生元素或自定义输入组件之间的桥梁。一旦我们的组件实现了该接口,我们需要告诉 Angular 通过提供它作为 NG_VALUE_ACCESSOR
来使用它。
在代码编辑器中重新查看 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); // 允许输入被禁用,并在禁用时使其略微透明。 @Input() disabled = false; @HostBinding('style.opacity') get opacity() { return this.disabled ? 0.25 : 1; } // 当评分发生变化时调用的函数。 onChange = (rating: number) => {}; // 当输入被触摸时(点击星星时)调用的函数。 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); } } // 允许 Angular 更新模型(评分)。 // 在这里更新模型和视图所需的更改。 writeValue(rating: number): void { this.stars = this.stars.map((_, i) => rating > i); this.onChange(this.value); } // 允许 Angular 注册一个在模型(评分)更改时调用的函数。 // 将函数保存为以后调用的属性。 registerOnChange(fn: (rating: number) => void): void { this.onChange = fn; } // 允许 Angular 注册一个在输入被触摸时调用的函数。 // 将函数保存为以后调用的属性。 registerOnTouched(fn: () => void): void { this.onTouched = fn; } // 允许 Angular 禁用输入。 setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } }
此代码将允许输入被禁用,并在禁用时使其略微透明。
运行应用程序:
ng serve
并在 Web 浏览器中进行交互。
您还可以禁用输入控件:
<rating-input [disabled]="true"></rating-input>
现在我们可以说我们的 RatingInputComponent
是一个自定义表单组件!它将在模板驱动或响应式表单中像任何其他原生输入一样工作(Angular 为这些提供了 ControlValueAccessors
!)。
结论
在本文中,您将一个基本的星级评分输入组件转换为了 ControlValueAccessor
。
现在您会注意到:
ngModel
现在可以正常工作。- 我们可以添加自定义验证。
- 控件状态和有效性可以通过
ngModel
获得,比如ng-dirty
和ng-touched
类。
如果您想了解更多关于 Angular 的知识,请查看我们的 Angular 专题页面,了解练习和编程项目。