Skip to content

Signal Forms: Using @for(userForm of valueForm.users; track userForm.id().value()) causes runtime error #66796

Description

@ptandler

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

I discovered this while trying to find a good solution how to edit a list of users with signal forms, which actually does not sound complicated, but it seems that there are things to consider I might not yet be aware of. Please see https://stackoverflow.com/questions/79878016/angular-21-signal-forms-how-to-loop-over-an-array-of-complex-sub-objects for the whole story.

In the 3rd example I described there, I tried to add a id to each user and use this for tracking in the loop like this. (StackBlitz Example 3)

interface User { id: number; name?: string; }
interface UserForm { id: number; name: string; }
interface Test { test: string; users: User[]; }

let userCounter = 0;
function createUser(name?: string): User { return { id: ++userCounter, name }; }

@Component({
  selector: 'user-edit',
  template: `<div>Name: <input type="text" [formField]="valueForm.name"/></div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FormField],
})
export class UserEditComponent implements FormValueControl<User | null> {
  public readonly value = model<User | null>(null);
  protected readonly valueForm = mappedForm<User | null, UserForm>(this.value, {
    modelToForm: (user) => {
      user ??= createUser('new user');
      return { id: user.id, name: user.name ?? '(unnamed))' };
    },
    formToModel: (userForm) => ({ id: userForm.id,name: userForm.name || undefined, }),
  });
}

@Component({
  selector: 'app-root',
  template: `
    <!-- instead of $index use form's user ID for tracking -->
    @for(userForm of valueForm.users; track userForm.id().value()) {
      <p>{{ userForm().value() | json }}</p>
      <user-edit [formField]="userForm" />
    }

    <button (click)="addUser()">Add User</button>
  `,
  imports: [UserEditComponent, FormField, JsonPipe],
})
export class App {
  public readonly value = signal<Test>({ test: 'a', users: [createUser('a'), createUser('b')], });
  public readonly valueForm = form(this.value);

  addUser() {
    this.valueForm
      .users()
      .value.update((list) => [...list, createUser(`user ${list.length}`)]);
  }
}

I expected that this improved tracking, but instead I get a runtime error and adding or editing users does not work.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-3r7vqbdb?file=src%2Fmain.ts,src%2Fuser-edit.component.ts

Please provide the exception or error you saw

ERROR RuntimeError: NG01904: Orphan field, can't find element in array <root>.users
    at Object.computation (_structure-chunk.mjs:953:15)
    at Object.producerRecomputeValue (_effect-chunk.mjs:310:25)
    at producerUpdateValueVersion (_effect-chunk.mjs:102:8)
    at computed2 (_effect-chunk.mjs:271:5)
    at Object.computation (_structure-chunk.mjs:850:40)
    at Object.producerRecomputeValue (_effect-chunk.mjs:310:25)
    at producerUpdateValueVersion (_effect-chunk.mjs:102:8)
    at consumerPollProducersForChange (_effect-chunk.mjs:170:5)
    at producerUpdateValueVersion (_effect-chunk.mjs:98:45)
    at linkedSignalGetter (_linked_signal-chunk.mjs:17:5)

Please provide the environment you discovered this bug in (run ng version)

Angular CLI       : 21.1.1
Angular           : 21.1.1
Node.js           : 20.19.1
Package Manager   : pnpm 8.15.6
Operating System  : linux x64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/animations       │ 21.1.1            │ 21.1.1            │
│ @angular/build            │ 21.1.1            │ 21.1.1            │
│ @angular/cli              │ 21.1.1            │ 21.1.1            │
│ @angular/common           │ 21.1.1            │ 21.1.1            │
│ @angular/compiler         │ 21.1.1            │ 21.1.1            │
│ @angular/compiler-cli     │ 21.1.1            │ 21.1.1            │
│ @angular/core             │ 21.1.1            │ 21.1.1            │
│ @angular/forms            │ 21.1.1            │ 21.1.1            │
│ @angular/platform-browser │ 21.1.1            │ 21.1.1            │
│ @angular/router           │ 21.1.1            │ 21.1.1            │
│ rxjs                      │ 7.8.2             │ ^7.8.1            │
│ typescript                │ 5.9.3             │ ^5.9.3            │
└───────────────────────────┴───────────────────┴───────────────────┘

Anything else?

Maybe the docs part about signal forms working with arrays could be enhanced a little bit to give also examples for this case.

Metadata

Metadata

Assignees

Type

No type

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions