Search code examples
angularformsangular-validationcontrolvalueaccessorangular-controlvalueaccessor

How to create custom input component with validation in Angular14?


I want to create custom component for input text but I don't know how can I bind validation of each input field to custom component. Is there any way to set errors of each field as array of objects like below

<app-custom-input
          formControlName="username"
          label="Username"
          [errors]="[
          { type: 'required', message: 'is required' },
          { type: 'minlength', message: 'min length error' }
        ]"
        ></app-custom-input>

In fact, I want to write reusable components that have different validations, for example, one field is required and another field has a length condition.

stackblitz.io


Solution

  • Step by step.

    When we make a custom form control with validate the function validate should return or an object or null -if no errors-.

      public validate(c: FormControl) {
        const errors: any[] = [];
        if (!this.control) this.control = c;
    
        this.errors &&
          this.errors.forEach((error) => {
            if (error.type == 'required') {
              if (!c.value) {
                errors.push({ required: true, message: error.message });
              }
            }
          });
        return errors.length ? { error: errors } : null;
      }
    

    See that return an object with an unique property "error" that is an array of errors.

    This error is an error not of the inner control -you input inside the custom-input- else the FormControl declared in "parent". So, to iterate over this error inside the custom form control we need "get it".

    Generally you use the constructor

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

    But, as we have a validate function simply we declare, and give value to it in validate function

      control!: AbstractControl;
    
      public validate(c: FormControl) {
        if (!this.control) this.control = c;
        ...rest of the code..
      }
    

    The last is iterate over this errors

    <ng-container *ngIf="control?.touched || control?.dirty">
      <ng-container *ngFor="let error of control?.errors?.error">
        <div class="error-text">
          {{ error.message }}
        </div>
      </ng-container>
    </ng-container>
    

    Your forked stackblitz

    NOTE: See that you needn't add the validators to your FormControls

    In your code:

    registrationForm = this.fb.group({
        username: [''],
        password: [''],
      });
    

    update to only send if reuired we need use some like

      public validate(c: FormControl) {
        const errors: any[] = [];
        if (!this.control) this.control = c;
    cons errorRequired==!c.value && this.errors?this.errors.find(x=>x=='required'):null
     if (errorRequired)
         return [{required:true,message:errorRequired.message}]
        
        this.errors &&
          this.errors.forEach((error) => {
            if (error.type == 'required') {
              if (!c.value) {
                errors.push({ required: true, message: error.message });
              }
            }
          });
        return errors.length ? { error: errors } : null;