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