14

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.

Martin
  • 1,877
  • 5
  • 21
  • 37

4 Answers4

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;
    }
  }
}
Martin
  • 1,877
  • 5
  • 21
  • 37
  • 8
    How would you go about accessing the input component instance from the validator function? Is it even possible? – Max Mumford Apr 05 '18 at 08:36
  • @MaxMumford see my answer below – Dmitry Efimenko May 20 '21 at 06:53
  • 1
    @DmitryEfimenko How does your answer help in this case as one cannot use dependency injection to get the injector because the validator function is defined outside of a class so it has no constructor? – Chris Apr 26 '22 at 08:57
3

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;
  }
}
Dmitry Efimenko
  • 10,973
  • 7
  • 62
  • 79
  • I tried this but in my case the `validate` function is simply never being called and validation does not work. Any idea, why? – Chris Apr 26 '22 at 08:32
  • 1
    checking for basics: did you declare the new Directive in the `declarations` of your module? Does the constructor execute? Put a console.log statement there to check – Dmitry Efimenko Apr 27 '22 at 09:13
  • I created the Directive automatically via VS Code so I don't know if it was automatically added to the `declarations` or not. I cannot check this anymore because I was already able to solve the problem with @Martin's suggestion in the meanwhile. Nevertheless, thanks for the help! – Chris Apr 27 '22 at 09:54
0

A clean of doing this is to create a @Directive with the same selector as the @Component. This way enables the @Directive to mirror any @Input the @Component has and account for it when validating.

@Directive({
  selector: 'fe-phone-number-input',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: PhoneNumberInputValidatorDirective,
      multi: true
    }
  ]
})
export class PhoneNumberInputValidatorDirective implements Validator { ... }
blid
  • 971
  • 13
  • 22
0

I'm not sure you need to implement the validator interface. For my custom controls I use the injected ngControl for any validation.

@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
{

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
  }

  updatePhone(phone: string) {
    this.value = phone;
    this.ngControl.control?.updateValueAndValidity();
  } 
}
<input
  class="phone-number-input mat-input-element"
  type="text"
  (blur)="updateValue(phoneInput.value)"
  [required]="required"
  #phoneInput
/>
JayChase
  • 11,174
  • 2
  • 43
  • 52