I'm trying to create custom form control by implementing MatFormFieldControl, ControlValueAccessor and Validator interfaces.
However, when I provide NG_VALUE_ACCESSOR
or NG_VALIDATORS
..
@Component({
selector: 'fe-phone-number-input',
templateUrl: './phone-number-input.component.html',
styleUrls: ['./phone-number-input.component.scss'],
providers: [
{
provide: MatFormFieldControl,
useExisting: forwardRef(() => PhoneNumberInputComponent)
},
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PhoneNumberInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PhoneNumberInputComponent),
multi: true
}
]
})
export class PhoneNumberInputComponent implements MatFormFieldControl<string>,
ControlValueAccessor, Validator, OnDestroy {
...
}
cyclic dependencies are created:
Uncaught Error: Template parse errors: Cannot instantiate cyclic dependency! NgControl
This works:
@Component({
selector: 'fe-phone-number-input',
templateUrl: './phone-number-input.component.html',
styleUrls: ['./phone-number-input.component.scss'],
providers: [
{
provide: MatFormFieldControl,
useExisting: forwardRef(() => PhoneNumberInputComponent)
}
]
})
export class PhoneNumberInputComponent implements MatFormFieldControl<string>,
ControlValueAccessor, Validator, OnDestroy {
...
constructor(@Optional() @Self() public ngControl: NgControl) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
}
But I still cannot figure out how to make validation work. Providing NG_VALIDATORS
creates cyclical dependency. Without providing it, validate
method is simply not called.
I'm using @angular/material 5.0.4.
To get rid of cyclical dependency, I removed the Validator
interface from the component and instead provided the validator function directly.
export function phoneNumberValidator(control: AbstractControl) {
...
}
@Component({
selector: 'fe-phone-number-input',
templateUrl: './phone-number-input.component.html',
styleUrls: ['./phone-number-input.component.scss'],
providers: [
{
provide: MatFormFieldControl,
useExisting: forwardRef(() => PhoneNumberInputComponent)
},
{
provide: NG_VALIDATORS,
useValue: phoneNumberValidator,
multi: true
}
]
})
export class PhoneNumberInputComponent implements MatFormFieldControl<string>,
ControlValueAccessor, OnDestroy {
...
constructor(@Optional() @Self() public ngControl: NgControl) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
}
My solution takes the idea from @blid, but rather duplicating the same @Inputs
as the component that's being validated has, I inject the component via dependency injection like so:
@Directive({
selector: 'fe-phone-number-input, [fePhoneNumber]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: PhoneNumberInputValidatorDirective,
multi: true
}
]
})
export class PhoneNumberInputValidatorDirective implements Validator {
constructor(private injector: Injector) {}
validate(control: FormControl) {
// use @Self to get the only instance of component that this validator is directly attached to
// use @Optional so that this validator can be used separately as a directive via attribute `fePhoneNumber`
const phoneNumberInputComponent = this.injector.get(PhoneNumberInputComponent, undefined, InjectFlags.Self | InjectFlags.Optional);
if (phoneNumberInputComponent?.myInput) {
// some custom logic
}
return null;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With