Reputation: 3309
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:
When the last checkbox is selected, I want to deselect all the other checkboxes.
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
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
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
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
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