Eddy Freeman
Eddy Freeman

Reputation: 3309

Issues with Angular FormArrays: Valuechanges, Validators and deselecting Checkboxes

I have Angular formArray of checkboxes. I also have a validator that makes sure at least one of the checkboxes is selected.

My problems are:

  1. When the last checkbox is selected, I want to deselect all the other checkboxes.

  2. When the user again select any of the checkboxes (except the last one), the last checkbox (which is already selected) must be deselected.

IMPORTANT: The user can select either the last checkbox or any number of other checkboxes. For instance if he selects the last checkbox and then click on any of the other checkboxes, then the last checkbox must be deselected.

The validator works perfect but when the last checkbox is selected, the other selected checkboxes are not deselected.

Though I think I have done the right thing but my browser console tells me that there are too many recursions and because of that it is not working.

The following link is my Stackblitz code where you can test the feature and see the source code.

(Click the Project icon in Stackblitz to open the file browser to see all the files).

Upvotes: 0

Views: 748

Answers (4)

Eliseo
Eliseo

Reputation: 57939

You can simplify the problem if use pairwise to know the check-box that has changed

private validateBooks(control: AbstractControl, errorKey: string) {
    control.valueChanges.pipe(startWith(control.value), pairwise()).subscribe(
      ([old, value]) => {
        let index = -1;
        value.forEach((x, i) => {
          if (old[i] != x)
            index = i;
        })
        if (index >= 0) {
          if (index == value.length - 1) {
            if (value[index])//if selected the last one
            {
              (this.booksForm.get('books') as FormArray).controls.forEach(
                (c, index) => {
                  if (index < value.length - 2)
                    c.setValue(false, { emit: false })
                })
            }
          } else {
            if (value[index]){  //if has selected another
               (this.booksForm.get('books') as FormArray).at(value.length - 1)
                 .setValue(false, { emit: false })
            }
          }
        }
      }
    )
  }

Upvotes: 1

Alex Vovchuk
Alex Vovchuk

Reputation: 2926

The trouble is in:

  if (lastBook === true) {
    this.deselectBooks(control);
  }

You get loop: deselectBooks -> executes value update -> executes subscribed validator and again deselectBooks.

Don't update false value to false. Only in case when it is required. To reach it update deselectBooks to:

  private deselectBooks(control: AbstractControl) {
      const formArray = control as FormArray;
      formArray.controls.map((book, index) => {
        let bookEntry = formArray.at(index);
        if(index !== (formArray.length - 1) && bookEntry.value) {
          book.setValue(false)
        }
      });
  }

And validateBooks:

  private validateBooks(control: AbstractControl, errorKey: string) {
    const books = control as FormArray;
    const lastBook = books.at(books.length - 1)
    control.valueChanges.subscribe(value => {
      control.setValidators(this.booksValidator(errorKey));
    });

    for(let entry of books.controls) {
      if (entry !== lastBook) {
        entry.valueChanges.subscribe(value => {
      if(value === true && lastBook.value) {
        lastBook.setValue(false);
      }
    });
      }
    }
    lastBook.valueChanges.subscribe(value => {
      if (value === true) {
        this.deselectBooks(control);
      }
    });
  }

https://stackblitz.com/edit/angular-6kprus?file=src/app/app.component.ts

Upvotes: 0

Andrew Allen
Andrew Allen

Reputation: 8002

The main issue here (after realising you need {emitEvent: false} when you setValue is that you need not just the checkbox values but the previous one to know which one changed.

This can be acheived with the RxJs pipeable operator scan

get bookArray() {
  return this.booksForm.controls['books'] as FormArray
}
this.bookArray.valueChanges.pipe(
    tap((val) => console.log(val)), // e.g. [false, false, true, false, false]
    distinctUntilChanged(),
    scan((acc, curr) => !acc && curr[curr.length - 1], false), // True only if None of the above has been selected
    tap((val) => console.log(val)),  // e.g. true or false
    ).subscribe((lastBookSelected: Boolean)=> {
      if (lastBookSelected) {
        this.bookArray.controls.forEach(control => {
          this.deselectBooks(control)
          console.log('here')
        })
      } else {
         // set last control false but use `{emitEvent: false}`
      }
    })

Note {emitEvent: false} is required on deselectBooks:

  private deselectBooks(control: AbstractControl) {
      this.bookArray.controls.map((book, index) => index !== (this.bookArray.length - 1) ? book.setValue(false,  {emitEvent: false}) : null);
  }

Stackblitz - https://stackblitz.com/edit/angular-tx5cpe

Upvotes: 2

Anil Kumar Reddy A
Anil Kumar Reddy A

Reputation: 648

The happens because on check of last check box value changes gets triggered which in turn makes an infinite loop.

Here is one way how can achieve.

 private deselectBooks(control: AbstractControl) {
      const formArray = control as FormArray;
      formArray.controls.map((book, index) => index !== (formArray.length - 1) ? book.setValue(false,{emitEvent:false}) : null); <--emit event as false
  } 

making emitEvent as false won't trigger valueChanges.

Hope it helps!

Upvotes: 0

Related Questions