Search code examples
angularvalidationangular-directiveangular2-directives

Angular form status validator


I'm trying to write custom form validator. I have this test component, where are three checkboxes. Main form should be valid only if at least one checkbox is checked.

https://stackblitz.com/edit/stackblitz-starters-69q3rq

There are two components - main-form, and inside is options component (this is just for testing purposes)

I set form validator (in main-form component) like this: appFormStatusValidator [isInvalid]="isInvalid">

and handle value of isInvalid property when any checkbox value is changed.

But inside of validation, the isValid property is false, even if property on the form is true (and vice versa).

Could you please tell me, what I'm doing wrong?

Thanks a lot.

I expect, that button will be enabled only if at least one checkbox is checked, and button will be disabled when all three checkboxes are unchecked.


Solution

  • I removed a lot of code, like the event binding and all the invalid flags, I just provided an input to the directive called toValidateFieldNames which contains the checkbox form field names to validate, and when we do the validation, we use the array operation some which checks if any of the provided elements in the array are true, then it returns true, which I inverted using ! to achieve the functionality you wanted, also when you use ControlContainer then is no need for extra form validations, the checkboxes in the child component are automatically detected by the parent!

    Since we are updating the form values, the validator get called on value changes, so there is no need for the events like change on the checkboxes!

    Below is a working example for your reference!

    directive

    import { Directive, Input } from '@angular/core';
    import {
      NG_VALIDATORS,
      AbstractControl,
      ValidationErrors,
      FormGroup,
    } from '@angular/forms';
    
    @Directive({
      selector: '[appFormStatusValidator]',
      providers: [
        {
          provide: NG_VALIDATORS,
          useExisting: FormStatusValidatorDirective,
          multi: true,
        },
      ],
    })
    export class FormStatusValidatorDirective {
      @Input() toValidateFieldNames: Array<string> = [];
    
      validate(control: AbstractControl): ValidationErrors | null {
        const formGroup = control as FormGroup;
        console.log(formGroup);
        // Your custom validation logic
        if (formGroup && !this.toValidateFieldNames?.some((field: string) => formGroup.value[field])) {
          return { formStatusInvalid: true }; // Form status is invalid
        } else {
          return null;
        }
      }
    }
    

    options.com.ts

    import { Component, Input, OnInit } from '@angular/core';
    import { ControlContainer, FormGroup, NgForm } from '@angular/forms';
    import { DataInterface } from '../data.interface';
    
    @Component({
      selector: 'app-options',
      templateUrl: './options.component.html',
      styleUrls: ['./options.component.css'],
      viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
    })
    export class OptionsComponent implements OnInit {
      @Input() item: DataInterface | null = null;
      formGroup!: FormGroup;
    
      constructor(private controlContainer: ControlContainer) {
        this.formGroup = this.controlContainer.control as FormGroup;
      }
    
      ngOnInit() {}
    }
    

    options.com.html

    <div>
      <span>is invalid (inside option component): {{ formGroup.valid }}</span>
    </div>
    <br />
    <ng-container *ngIf="item">
      <div>
        <mat-checkbox name="option1" [(ngModel)]="item.option1"
          >Option1</mat-checkbox
        >
      </div>
      <br />
      <div>
        <mat-checkbox name="option2" [(ngModel)]="item.option2"
          >Option2</mat-checkbox
        >
      </div>
      <br />
      <div>
        <mat-checkbox name="option3" [(ngModel)]="item.option3"
          >Option3</mat-checkbox
        >
      </div>
    </ng-container>
    

    main form.com.ts

    import { Component, OnInit, ViewChild } from '@angular/core';
    import { NgForm } from '@angular/forms';
    import { DataInterface } from '../data.interface';
    
    @Component({
      selector: 'app-main-form',
      templateUrl: './main-form.component.html',
      styleUrls: ['./main-form.component.css'],
    })
    export class MainFormComponent implements OnInit {
      constructor() {}
    
      @ViewChild('inputForm') inputForm?: NgForm;
    
      item: DataInterface = { id: 1, name: 'test' };
    
      ngOnInit() {}
    }
    

    main form.com.html

    <div>
      {{ item | json }}
    </div>
    
    
    <form #inputForm="ngForm"
    appFormStatusValidator [toValidateFieldNames]="[
    'option1',
    'option2',
    'option3',
    ]">
      <hr/>
    
      <app-options [item]="item"/>
    
      <hr/>
      <div>
        <span>is invalid (main component): {{ inputForm.form.invalid }}</span>
      </div>
      <div>
        <span>form status: {{ inputForm.form.status}}</span>
      </div>
      <div>
        <span>form errors: {{ inputForm.form.errors | json}}</span>
      </div>
      <hr/>
      <button [disabled]="inputForm.form.invalid">
        Submit
      </button>
    
    </form>
    

    Stackblitz Demo