Angular FormArrays 的问题:Valuechanges、验证器和取消选择复选框

Issues with Angular FormArrays: Valuechanges, Validators and deselecting Checkboxes

我有 Angular 个 formArray 复选框。我还有一个验证器,可确保至少选中一个复选框 selected。

我的问题是:

  1. 当最后一个复选框被 select 编辑时,我想取消select 所有其他复选框。

  2. 当用户再次select任何复选框(最后一个除外)时,最后一个复选框(已经selected)必须取消select编辑。

重要提示: 用户可以 select 最后一个复选框或任意数量的其他复选框。例如,如果他 select 选择最后一个复选框,然后单击任何其他复选框,则必须取消 select 最后一个复选框。

验证器工作完美,但是当最后一个复选框被 selected 时,其他 selected 复选框不会被取消selected。

虽然我认为我做对了,但我的浏览器控制台告诉我递归太多,因此无法正常工作。

以下 link 是我的 Stackblitz 代码,您可以在其中测试功能并查看源代码。

(单击 Stackblitz 中的项目图标打开文件浏览器以查看所有文件)。

问题出在:

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

你得到循环: deselectBooks -> 执行值更新 -> 执行订阅的验证器并再次 deselectBooks.

不要将 false 值更新为 false。仅在需要时使用。 要到达它,请将 deselectBooks 更新为:

  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)
        }
      });
  }

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

发生这种情况是因为在检查最后一个复选框时触发了值更改,这反过来又造成了无限循环。

这是一种实现方式。

 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
  } 

将 emitEvent 设置为 false 不会触发 valueChanges。

希望对您有所帮助!

这里的主要问题(意识到当你 setValue 时你需要 {emitEvent: false} 是你不仅需要复选框值,还需要前一个复选框值来知道哪个更改了。

这可以通过 RxJs 管道运算符实现 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}`
      }
    })

注意 {emitEvent: false}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

如果使用 pairwise 知道发生变化的复选框,可以简化问题

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 })
            }
          }
        }
      }
    )
  }