Synchronize the state of validation for custom control with inner PrimeNG control in Angular

Viewed 207

I am trying to create a wrapper object around an PrimeNG Calendar control and after years not developing with Angular I still have to note, that there is no easy way in extending a control and all it's control-functionality.

What I want is, that the necessary functionality (like setting the validation state, eg. "ng-pristine" or "ng-dirty") is passed to the inner control. But that doesn't work correctly. Maybe there is someone, who has solved this problem!

const noop = () => {
};

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CalendarFieldComponent),
  multi: true
};

@Component({
  selector: 'calendar-field',
  templateUrl: './calendar-field.component.html',
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR],

})
export class CalendarFieldComponent implements ControlValueAccessor, Validator {

  @Input() required = false;
  @ViewChild(NgModel) model?: NgModel;

  private innerValue: any = '';

  private onTouchedCallback: () => void = noop;
  private onChangeCallback: (_: any) => void = noop;

  private onValidatorChange: (_: any) => void = noop;

  get value(): any {
    return this.innerValue;
  };

  set value(v: any) {
    console.log(this.model);
    this.model?.control.markAsDirty();
    if (v !== this.innerValue) {
      this.innerValue = v;
      this.onChangeCallback(v);
    }
  }

  onBlur() {
    this.onTouchedCallback();
  }

  writeValue(value: any) {
    if (value !== this.innerValue) {
      this.innerValue = value;
    }
  }

  registerOnChange(fn: any) {
    this.onChangeCallback = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouchedCallback = fn;
  }

  validate(control: AbstractControl): ValidationErrors | null {
    return {'required': true};
  }

  registerOnValidatorChange(fn: () => void): void {
    this.onValidatorChange = fn;
  }
}

The template looks like the following:

<p-calendar [(ngModel)]="value" styleClass="inputfield w-full" appendTo="body" [required]="required" (blur)="onBlur()"></p-calendar>

When I have a FormGroup which contains the control above. When the Form is submitted, I want to validate all field, which already works by using the following code. All the empty fields have a red border (because of ng-invalid), except for the wrapped controls.

// code which validates all the fields when clicking on submit button
private validateForm(): boolean {
    for (const controlKey of Object.keys(this.form.controls)) {
        const control = this.form.controls[controlKey];
        control.markAllAsTouched();
        control.markAsDirty();
        control.markAsTouched();
    }
    return !!this.form.valid;
}

The wrapper control gets the classes "ng-invalid ng-touched ng-dirty" whereas the inner control just get the classes "ng-untouched ng-pristine ng-invalid ng-star-inserted".

How can I synchronize this (from wrapper to inner)?

1 Answers

I'm doing something like this to sync the errors from the inner control with the outer control

validate(control: AbstractControl): ValidationErrors | null {
    return {'required': this.validateForm()};
}

To achieve the other way around (set error on the inner control) I do something like this:

const ngControl = this.injector.get(NgControl).control;
ngControl.statusChanges.pipe(filter(x => x === 'INVALID' || x === 'VALID' )).subscribe(_ => this.setInputErrors())

setInputErrors(): void {
    const ngControl = this.injector.get(NgControl).control;
    if (ngControl.invalid) {
        this.innerFormControl.setErrors( { incorrect: true });
    else {
        this.innerFormControl.setErrors(null);
    }
}

For this solution, I'm using a reactive forms approach instead of template driven. This works in most cases for me, but you would probably make some changes.

Related