#Asynchronously load elements of a reactive form array

25 messages · Page 1 of 1 (latest)

deft vapor
#

Hi everyone. I want to asynchronously populate a dynamic form array, showed in this example:

import { Component, signal } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay, finalize } from 'rxjs/operators';
import { OnInit, Injectable, inject, effect } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormControl } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [ReactiveFormsModule],
  template: `
    <div class="h-screen flex flex-col items-center justify-center space-y-2">
      @for(control of form.controls; track $index) {
        <label>Name:
        <input type="text" [formControl]="control" class="border border-gray-400 rounded-sm ms-2"/>
        </label>
      }
    </div>
  `
})
export class App implements OnInit {
  private store = inject(MailStore);
  private fb = inject(FormBuilder);
  protected form = this.fb.array<FormControl<string | null>>([]);

  constructor() {
    effect(() => {
      const attachments = this.store.attachments();
      if (attachments == null) return;
      attachments.forEach(att => this.form.push(new FormControl(att)));
    });
  }

  ngOnInit() {
    this.store.loadAttachments();
  }
}

@Injectable({providedIn: "root"})
export class MailStore {
  attachments = signal<string[] | null>(null);

  loadAttachments(): void {
    this.getAttachments().subscribe({
      next: value => this.attachments.set(value)
    });
  }

  getAttachments(): Observable<string[]> {
    return of(["Attachment 1", "Attachment 2", "Attachment 3"])
      .pipe(delay(2400));
  }
}

But I get the error:

ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '[object Object]'

How to populate a form array with data from an asynchronous source?

north bear
#

Without looking too deep, try afterRenderEffect instead just effect

deft vapor
#

Thanks for your reply!
I tried using afterRenderEffect instead of effect, but it still results in the same ExpressionChangedAfterItHasBeenCheckedError.

last vapor
last vapor
dawn edge
#

The smallest repro

@Component({
  selector: 'app-root',
  template: `{{ val }}`,
})
export class App {
  val: string | undefined;
  readonly source = signal<string | undefined>(undefined);

  constructor() {
    effect(() => (this.val = this.source()));

    setTimeout(() => this.source.set('a'));
  }
}
last vapor
#

So, is it an Angular bug? It seems strange to me that Angular knows a CD is needed due to the effect, but then claims that we have modified the value of an expression during CD (which we haven't).

dawn edge
#

There is something strange yes.

#

Ah I think I know what happens. It knows the effect is dirty so it runs it but also it knows the template isn't

#

(or at least it shouldn't)

#

The template isn't dirty, yet there is a change => hence the error

last vapor
#

Well, when using with reactive forms, since it's not based on signals, it's perfectly normal to have no signal read in the template but still want something different to be displayed.

dawn edge
#

Well you're mutating something

#

here the form.controls array.

#

And you're doing this in an effect

#

If you're in an effect don't mutate anything that is used in the template, that would seem reasonable to me.

last vapor
#

How else would you modify a reactive form value/structure when a signal changes?

dawn edge
#

markForCheck()

#

to mark the template dirty

#

I knew we had a related issue, forgot it was yours 😄

last vapor
#

Yes, I thought about that too, but my issue was that there was no CD. Here there is a CD complaining about an expression being changed during CD.
markForCheck indeed works, which is nice. But it's counter-intuitive to tell Angular to do a CD, when the error is about a CD that it does all by itself.

dawn edge
#

effect is part of CD

deft vapor
#

I'll use the signal in the template instead of using markForCheck() because i need to display the length of the form array.