我正在 Angular (16) 中开发我的第一个自定义控件,用于反应式表单,其中包含滑块和输入。如果滑块为真,则输入应该有一个值。如果不是,则输入不应该有值。
自定义组件 HTML:
<div class="benefit-field">
<div class="slide-toggle">
<mat-slide-toggle [checked]="slideEnabled" (change)="updateEnabled($event)">Enabled?</mat-slide-toggle>
</div>
<div [ngClass]="{'disabled': !slideEnabled}">
<mat-form-field>
<mat-label>{{ typeName }}</mat-label>
<input type="text" matInput [value]="valueId" [disabled]="!slideEnabled" (change)="onValueChanged($event.target)" />
</mat-form-field>
</div>
</div>
自定义组件 TypeScript:
import { Component, forwardRef, Input } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
@Component({
selector: 'app-optional-benefit-field',
templateUrl: './optional-benefit-field.component.html',
styleUrls: ['./optional-benefit-field.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: OptionalBenefitFieldComponent
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => OptionalBenefitFieldComponent)
},
]
})
export class OptionalBenefitFieldComponent implements ControlValueAccessor, Validator {
@Input() typeId: number;
@Input() typeName: number;
valueId: number;
slideEnabled = false;
onChange = (_valueId) => { /* nothing needs to be done here, let it trickle up */ };
onTouched = () => { /* nothing needs to be done here, let it trickle up */ };
touched = false;
disabled = false;
writeValue(valueId: number): void {
if (valueId && valueId > 0) {
this.slideEnabled = true;
}
this.valueId = valueId;
}
registerOnChange(onChange: any): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: any): void {
this.onTouched = onTouched;
}
markAsTouched(): void {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
updateEnabled(event: MatSlideToggleChange): void {
this.markAsTouched();
this.slideEnabled = event.checked;
if (!this.slideEnabled) {
this.onChange(null);
} else {
this.onChange(this.valueId);
}
}
onValueChanged(input: HTMLInputElement): void {
if (input.value && (+input.value) > 0) {
this.onChange(+input.value);
} else {
this.onChange(null);
}
}
validate(control: AbstractControl): ValidationErrors | null {
return (!this.slideEnabled || (this.slideEnabled && control.value))
? null
: { required: true };
}
}
父组件HTML:
<app-optional-benefit-field
[typeId]="..."
[typeName]="..."
formControlName="valueId"
></app-optional-benefit-field>
当我将自定义控件置于无效状态时,我可以看到
validate
返回 { required: true }
。
但是,如果我查看父窗体,
.value
具有当前控件的值,但 .errors
为空。
我的自定义控件或父组件中缺少什么才能正确发送/接收验证错误?
这看起来像是角度发布中的一个小错误,证明。如果你愿意,你可以提出一个 github 问题。解决方案是将您的 Angular 版本降低到最新版本,例如 18。
无论如何,代码需要进行以下更改。
当你使用表单字段模块时,它必须有
NgControl
,这可以由ngModel
或formControl
提供,我使用的是formControl
。
使用反应式表单时不要使用
[disabled]
属性,因此我将其更改为编程式。
我将输入更改为数字,因为您只为数字编写了一些检查。
除此之外,你的代码运行得很好。
import { Component } from '@angular/core';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { bootstrapApplication } from '@angular/platform-browser';
import { forwardRef, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
AbstractControl,
ControlValueAccessor,
FormControl,
FormGroup,
FormsModule,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
ValidationErrors,
Validator,
Validators,
} from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'app-optional-benefit-field',
imports: [
MatFormFieldModule,
MatSlideToggleModule,
CommonModule,
MatInputModule,
FormsModule,
ReactiveFormsModule,
],
standalone: true,
template: `
<div class="benefit-field">
<div class="slide-toggle">
<mat-slide-toggle [checked]="slideEnabled" (change)="updateEnabled($event)">Enabled?</mat-slide-toggle>
</div>
<div [ngClass]="{'disabled': !slideEnabled}">
<mat-form-field>
<mat-label>{{ typeName }}</mat-label>
<input type="number" matInput [formControl]="valueId" (input)="onValueChanged($event)"/>
</mat-form-field>
</div>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: OptionalBenefitFieldComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => OptionalBenefitFieldComponent),
},
],
})
export class OptionalBenefitFieldComponent
implements ControlValueAccessor, Validator
{
@Input() typeId!: number;
@Input() typeName!: number;
valueId: FormControl<number | null> = new FormControl<number | null>({
value: null,
disabled: true,
});
slideEnabled = false;
onChange = (_valueId: any) => {
/* nothing needs to be done here, let it trickle up */
};
onTouched = () => {
/* nothing needs to be done here, let it trickle up */
};
touched = false;
disabled = false;
writeValue(valueId: number): void {
if (valueId && valueId > 0) {
this.slideEnabled = true;
}
this.valueId.patchValue(valueId);
}
registerOnChange(onChange: any): void {
this.onChange = onChange;
}
registerOnTouched(onTouched: any): void {
this.onTouched = onTouched;
}
markAsTouched(): void {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
updateEnabled(event: MatSlideToggleChange): void {
this.markAsTouched();
this.slideEnabled = event.checked;
if (!this.slideEnabled) {
this.onChange(null);
this.valueId.disable();
} else {
this.onChange(this.valueId.value);
this.valueId.enable();
}
this.valueId.updateValueAndValidity();
}
onValueChanged(event: any): void {
const value = event?.target?.value;
if (value && +value > 0) {
this.onChange(+value);
} else {
this.onChange(null);
}
}
validate(control: AbstractControl): ValidationErrors | null {
return !this.slideEnabled || (this.slideEnabled && control.value)
? null
: { required: true };
}
}
/**
* @title Basic slide-toggles
*/
@Component({
selector: 'slide-toggle-overview-example',
template: `
<form [formGroup]="form">
<app-optional-benefit-field
[typeId]="1234"
[typeName]="1234"
formControlName="valueId"
></app-optional-benefit-field>
</form>
<hr/>
{{form.errors | json}}
<hr/>
{{form.controls.valueId.errors | json}}
<hr/>
{{form.value | json}}
`,
standalone: true,
imports: [OptionalBenefitFieldComponent, ReactiveFormsModule, CommonModule],
})
export class SlideToggleOverviewExample {
form = new FormGroup({
valueId: new FormControl(null),
});
}