Home > Net >  Angular 12 FormGroup dynamically Array checkboxes custom validator does not work
Angular 12 FormGroup dynamically Array checkboxes custom validator does not work

Time:04-30

I am creating an Angular 12 app, with Material.

I have a form with an checkbox array loaded dynamically from database.

I need to validate that at least one checkbox is selected

I defined like this in my OnInit():

 ngOnInit(): void {
    this.form = this.fb.group({
      Id: new FormControl(null),
      Name: new FormControl('',Validators.required),
      Recipents: new FormControl('',[Validators.required, matchingEmailValidator()]),
      IsActive: new FormControl(true),
      ProcessorName: new FormControl('',Validators.required),
      Channel: new FormArray([]),
    }, { validators: [customValidateArrayGroup()] }
    );
}

I need a custom validation for channel form array. If I added it in the definition of the channel, it does not fire when I check it. So, I decided to do it at the form level..

I added:

{ validators: [customValidateArrayGroup()] }

Every time an object changes, it fires this validator.

This is my custom validator:

export function customValidateArrayGroup(): ValidatorFn {
  return function validate(formGroup: FormGroup) {
    let checked = 0
    Object.keys(formGroup.controls).forEach(key => {
      const control = formGroup.controls[key]
      if (control.value) {
        checked  
      }
    })

    if (checked < 1) {
      return {
        requireCheckboxToBeChecked: true,
      }
    }
    return null
  }
}

Here is my Html where I defined the Checkbox Array

 <mat-label><strong>Channel</strong></mat-label>
          <li *ngFor="let chanel of notification.NotificationChannelLogLevels">
            <mat-checkbox id= {{chanel.NotificationLogLevel.Id}} formArrayName="Channel"
            [checked]="chanel.IsActive"
             (change)="changeEventFunc($event)">
              {{chanel.NotificationLogLevel.Name}}
            </mat-checkbox>
          </li>

The problem I have is that the custom validator does not fire when a checkbox is clicked. Maybe is becouse they are loaded dinamically and are not recognized by formGroup.controls

How can I validate this?

CodePudding user response:

I think you have your FormArray setup incorrectly in your template.

You are applying the formArrayName attribute the each checkbox when it needs to be applied to a parent container,

<div formArrayName="myFormArray">
  <div *ngFor="*ngFor="let chanel of notification.NotificationChannelLogLevels; let i = index">
    //Use the index here to dynamically tie each mat-checkbox to a FormControl
    <mat-checkbox [FormControl]="myCheckboxes[i]"></mat-checkbox>
  </div>
</div>

And then in your .ts file you'll have to define myCheckboxes as a FormArray with instances of form control inside it. Otherwise myCheckboxes[i] will be either null or an index out of bounds. You can use the form array you added to your form group, but the indexes you reference in the template have to be defined.

Here is a good blog post going over how to handle adding/removing instances from the form array,

https://netbasal.com/angular-reactive-forms-the-ultimate-guide-to-formarray-3adbe6b0b61a

And another,

https://blog.angular-university.io/angular-form-array/

As a side note, if your logging levels are static, it may just be easier or more intuitive to define the list of checkbox controls as a FormGroup and apply your validator to the form group.

CodePudding user response:

You have an odd mix of using formarray and your js array in the template. Currently your formarray is completely empty, so it would be expected that it does not run when checkboxes are checked. You can choose to iterate your JS array and push / remove to formarray, or then you push the values to the formarray when you receive the data and then just iterate that one in the template. The below solution does the latter:

Shortened code....

Build form:

this.form = this.fb.group({
  Channel: this.fb.array([], [customValidateArrayGroup()]),
});

I attached the custom validator to the formarray itself. When you have the dynamic data ready, then iterate it and push form controls to your formarray. I like to use a getter as well. Push whatever properties you need, here I choose IsActive and Name only:

get channels() {
  return this.form.get('Channel') as FormArray;
}

// when you have data accessible:
this.notificationChannelLogLevels.forEach(value => {
  this.channels.push(this.fb.group({
    isActive: value.IsActive,
    name: value.Name
  }))
})

Now iterate this formarray in the template:

<div formArrayName="Channel">
  <li *ngFor="let chanel of channels.controls; let i = index">
    <ng-container [formGroupName]="i">
      <mat-checkbox formControlName="isActive">
        {{ chanel.get('name').value}}
      </mat-checkbox>
    </ng-container>
  </li>
  <small *ngIf="channels.hasError('hasError') && channels.touched">
    Choose at least one
  </small>
</div>

The custom validator checks that at least one checkbox field has isActive as true:

export function customValidateArrayGroup() {
  return function validate(formArr: AbstractControl): ValidationErrors | null {
    const filtered = (formArr as FormArray).value.filter(chk => chk.isActive);
    return filtered.length ? null : { hasError: true }
  };
}

A STACKBLITZ for your reference.

  • Related