我们已经习惯了使用 <input>, <select> 等标准的表单控件。但如果,你的产品经理提出了一个「天马行空」的需求呢?
- 「我想要一个可以拖拽打分的星级评分控件!」
- 「这里需要一个能拾取颜色的调色盘!」
这时,你会发现,Angular 的内置控件已经不够用了。你需要的,是创造一个属于你自己的、能与 Angular 表单体系(无论是模板驱动还是响应式)无缝协作的自定义表单控件。
实现这一「独家定制」魔法的「秘密契约」,就是 ControlValueAccessor。
「翻译官」的使命:ControlValueAccessor 是什么?#
ControlValueAccessor (简称 CVA) 是一个接口。任何一个组件,只要实现了这个接口,就等于在告诉 Angular:「嘿,我虽然外表特立独行,但我懂得你们表单世界的『官方语言』!你可以像对待普通 <input> 一样对待我。」
比喻一下:FormControl 是一个标准的「PS5游戏手柄」,而你的「星级评分组件」是一个你亲手打造的、独一無二的「高达模型」。ControlValueAccessor 就是那个「万能转换器」,它负责将手柄的通用信号(「设置值」、「禁用」),翻译成高达能听懂的指令;同时,当用户手动掰动高达的机械臂时(用户交互),它又负责将这个动作,翻译成手柄能理解的「值已改变」的信号,并报告回去。

「万能转换器」的四个接口#
要实现这个「转换器」,你需要实现 ControlValueAccessor 接口中最多四个关键方法。
-
writeValue(obj: any): void-
方向:FormControl -> 你的组件
-
作用:当表单模型从外部被修改时(比如调用了
form.patchValue()),Angular 会调用此方法,并将新值obj传给你。你的任务,就是接收这个值,并用它来更新你组件的内部 UI。 -
翻译:「手柄说:目标值是 5。」
-
-
registerOnChange(fn: any): void-
方向:你的组件 -> FormControl
-
作用:在初始化时,Angular 会传给你一个回调函数
fn(我们通常叫它onChange)。你必须把它保存起来。当你的组件内部因为用户交互而导致值发生变化时,你必须调用这个保存好的onChange(newValue)函数,把新值报告给FormControl。 -
翻译:「手柄说:这是我的『报告专线』,有情况随时打给我。」
-
-
registerOnTouched(fn: any): void-
方向:你的组件 -> FormControl
-
作用:与
registerOnChange类似,Angular 会传给你另一个回调函数fn(我们通常叫它onTouched)。你应该在你的组件被视为「已接触」(通常是在blur事件或首次交互后)时,调用一次这个onTouched()函数。 -
翻译:「手柄说:这是我的『碰触』报告线,碰过了就打一声招呼。」
-
-
setDisabledState?(isDisabled: boolean): void(可选)-
方向:FormControl -> 你的组件
-
作用:当
FormControl的禁用状态被改变时(如调用form.disable()),Angular 会调用此方法。你可以在这里实现让你组件的 UI 呈现「禁用」样式的逻辑。 -
翻译:「手柄说:现在进入/解除禁用模式。」
-
实战:打造一个「星级评分」控件#
让我们从零开始,创建一个 <app-star-rating> 组件。
第一步:创建组件基础 UI
// star-rating.component.ts
@Component({
selector: 'app-star-rating',
standalone: true,
imports: [CommonModule],
template: `
<div class="rating-container">
@for (star of stars; track i; let i = $index) {
<span (click)="rate(i + 1)">
{{ i < rating ? '★' : '☆' }}
</span>
}
</div>
`,
styles: [`...`]
})
export class StarRatingComponent {
stars = [1, 2, 3, 4, 5];
rating = 0;
rate(newRating: number) {
this.rating = newRating;
}
}第二步:签署「魔法契约」
让我们的组件 implements ControlValueAccessor,并提供 NG_VALUE_ACCESSOR。
// star-rating.component.ts
import {forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
// ...
providers: [
{
provide: NG_VALUE_ACCESSOR,
// `useExisting` 和 `forwardRef` 是固定写法,告诉 DI 系统,「我」就是那个 CVA 的实现
useExisting: forwardRef(() => StarRatingComponent),
multi: true // 因为一个表单元素上可能还有其他 CVA,所以设为 multi
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
// ...
}第三步:实现接口方法
export class StarRatingComponent implements ControlValueAccessor {
stars = [1, 2, 3, 4, 5];
rating = 0;
// 保存 Angular 给我们的回调函数
onChange: (value: number) => void = () => {
};
onTouched: () => void = () => {
};
// 1. 实现 writeValue
writeValue(value: number): void {
this.rating = value ?? 0;
}
// 2. 实现 registerOnChange
registerOnChange(fn: any): void {
this.onChange = fn;
}
// 3. 实现 registerOnTouched
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
// 4. 修改我们自己的交互逻辑
rate(newRating: number) {
this.rating = newRating;
this.onChange(this.rating); // !! 关键:当内部值变化时,调用 onChange 报告给 FormControl
this.onTouched(); // 顺便标记为已接触
}
}第四步:像普通控件一样使用它!
现在,你的 <app-star-rating> 已经是一个合格的 Angular 表单控件了!
<!-- 在响应式表单中使用 -->
<form [formGroup]="myForm">
<app-star-rating formControlName="rating"></app-star-rating>
</form>
<!-- 在模板驱动表单中使用 -->
<app-star-rating name="rating" [(ngModel)]="myRating"></app-star-rating>结语#
ControlValueAccessor 是连接你天马行空的创造力与 Angular 强大表单工程体系之间的「魔法桥梁」。掌握了它,你就能将任何复杂的交互组件,都驯服成表单世界里温顺、可靠的一员。
从此,你不再仅仅是表单控件的「消费者」,更是「创造者」。那些为你的应用量身打造的、独一无二的输入体验,正等待着你用这把「钥匙」去开启。
正如古人所言:「工欲善其事,必先利其器。」意指工匠想要做好工作,必须首先使他的工具精良。掌握 ControlValueAccessor,正是我们前端工程师「利其器」的重要一步,它让我们的「工具箱」不再受限于标准,而是能够根据实际需求,「独家定制」最趁手的「神兵利器」,从而更好地「善其事」。