Home > Software engineering >  Dynamically compute value from multiple FormControl valueChanges with rxjs in Angular
Dynamically compute value from multiple FormControl valueChanges with rxjs in Angular

Time:12-02

I have an array of FormGroups which all holds one FormControl called checked which is represented as a checkbox input in the template.

This array formGroups$ is computed from another Observable called items$.

// component.ts
  constructor(private fb: FormBuilder) {}

  items$ = of([{ whatever: 'not used' }, { something: 'doesnt matter' }]);

  // doesn't work!
  formGroups$: Observable<FormGroup<{ checked: FormControl<boolean> }>[]> =
    this.items$.pipe(
      map((items) => {
        const array: FormGroup[] = [];
        for (const item of items) {
          const formGroup = this.fb.group({});
          formGroup.addControl('checked', new FormControl(false));
          array.push(formGroup);
        }
        return array;
      })
    );

  allChecked$: Observable<boolean> = this.formGroups$.pipe(
    switchMap((formGroups) => {
      const valueChangesArray: Observable<boolean>[] = [];
      formGroups.forEach((formGroup) => {
        valueChangesArray.push(
          formGroup
            .get('checked')
            .valueChanges.pipe(startWith(formGroup.get('checked').value))
        );
      });
      return combineLatest(valueChangesArray);
    }),
    map((checkedValues) => {
      console.log(checkedValues);
      for (const isChecked of checkedValues) {
        if (!isChecked) {
          return false;
        }
      }
      return true;
    })
  );
<!-- component.html -->
<ng-container *ngFor="let formGroup of formGroups$ | async; index as i">
  <label>
    <input type="checkbox" [formControl]="formGroup.controls.checked" />
    {{ i }}
  </label>
</ng-container>

<p>allChecked: {{ allChecked$ | async }}</p>

Example see also in Stackblitz: enter image description here

If I change formGroup$ to a simpler static solution, the value allChecked$ is computed correctly every time:

  // works!
  formGroups$: Observable<FormGroup<{ checked: FormControl<boolean> }>[]> = of([
    new FormGroup({
      checked: new FormControl(false),
    }),
    new FormGroup({
      checked: new FormControl(true),
    }),
  ]);

You can easily reproduce it in this StackBlitz: https://stackblitz.com/edit/angular-ivy-xfpywy?file=src/app/app.component.ts

How do I compute this boolean allChecked$ with an array of dynamically created FormGroups?

CodePudding user response:

It looks like the issue with your code is that you are using the of operator to create an observable from your items$ array, but of creates a cold observable that only emits a single value (the array) and then completes. As a result, your formGroups$ observable only emits a single value and never updates.

To fix this issue, you could use the from operator instead of of. The from operator creates an observable from an array (or any other iterable object), and it will emit the values in the array one at a time. This will allow your formGroups$ observable to update when the values in items$ change.

Here is an example of how you could update your code to use the from operator:

// component.ts
constructor(private fb: FormBuilder) {}

items$ = from([[{ whatever: 'not used' }, { something: 'doesnt matter' }]]);

formGroups$: Observable<FormGroup<{ checked: FormControl<boolean> }>[]> =
  this.items$.pipe(
    map((items) => {
      const array: FormGroup[] = [];
      for (const item of items) {
        const formGroup = this.fb.group({});
        formGroup.addControl('checked', new FormControl(false));
        array.push(formGroup);
      }
      return array;
    })
  );

allChecked$: Observable<boolean> = this.formGroups$.pipe(
  switchMap((formGroups) => {
    const valueChangesArray: Observable<boolean>[] = [];
    formGroups.forEach((formGroup) => {
      valueChangesArray.push(
        formGroup
          .get('checked')
          .valueChanges.pipe(startWith(formGroup.get('checked').value))
      );
    });
    return combineLatest(valueChangesArray);
  }),
  map((checkedValues) => {
    console.log(checkedValues);
    for (const isChecked of checkedValues) {
      if (!isChecked) {
        return false;
      }
    }
    return true;
  })
);

CodePudding user response:

You have two different arrays of formGroups!! I Imagine you can use ShareReply to not create a new formGroup each time you subscribe

But You can also use "tap" operator to create the allChecked$ Observable

  allChecked$: Observable<boolean>;
  formGroups$: Observable<FormGroup<{ checked: FormControl<boolean> }>[]> =
    this.items$.pipe(
      map((items) => {
        const array: FormGroup[] = [];
        for (const item of items) {
          const formGroup = this.fb.group({});
          formGroup.addControl('checked', new FormControl(false));
          array.push(formGroup);
        }
        return array;
      }),
      tap((formGroups: any[]) => {           //here we create the Observable
                  //see that "formGroups" are your array of FormGroups

        const valueChangesArray: Observable<boolean>[] = [];
        formGroups.forEach((formGroup) => {
          valueChangesArray.push(
            formGroup
              .get('checked')
              .valueChanges.pipe(startWith(formGroup.get('checked').value))
          );
        });
        this.allChecked$ = combineLatest(valueChangesArray).pipe(
          map((checkedValues) => {
            for (const isChecked of checkedValues) {
              if (!isChecked) {
                return false;
              }
            }
            return true;
          })
        );
      })
    );

Your forked stackblitz

  • Related