Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MatFormFieldControl that implements ControlValueAccessor and Validator creates cyclic dependency

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.

like image 678
Martin Avatar asked Feb 08 '18 09:02

Martin


2 Answers

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;
    }
  }
}
like image 116
Martin Avatar answered Sep 27 '22 17:09

Martin


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;
  }
}
like image 29
Dmitry Efimenko Avatar answered Sep 27 '22 18:09

Dmitry Efimenko