I'm working on my first custom control in Angular (16), for a Reactive Form, that contains a slider and an input. If the slider is true, then the input should have a value. If it is not, then the input shouldn't have a value.
Custom component 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>
Custom component 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 };
}
}
Parent component HTML:
<app-optional-benefit-field
[typeId]="..."
[typeName]="..."
formControlName="valueId"
></app-optional-benefit-field>
When I put the custom control into an invalid state, I can see that validate is returning a { required: true }.
If I look at the parent form, however, .value has the current control's values but .errors is null.
What am I missing in my custom control or parent component for it to properly send/receive the validation error?
FormControlconstructor. See the documentation for an example. I think you can still do it as you've written, you might need to make the function static though...new FormControl(null, [OptionalBenefitFieldComponent.validate]).