Search code examples
angularangular-reactive-forms

Dynamically removing a FormControl in a nested form (FormRecord) results in 'No overload matches this call'


I'm working with an angular app where I need to dynamically add/remove form controls (checkboxes) in a form record.

I created a minimal version of this behaviour on stackblitz, the important stuff is in the code below.

private formBuilder = inject(NonNullableFormBuilder);

form = this.formBuilder.record<boolean>({});

nestedForm = this.formBuilder.group({
  field1: [``],
  subRecord: this.formBuilder.record<boolean>({}),
});

// This works without problem
removeRandomFormControls() {
  const record = this.form;
  Object.keys(record.controls).forEach((key) => {
    if (Math.random() > 0.5) {
      return;
    }
    record.removeControl(key, { emitEvent: false });
  });
}

// This is the exact same thing, but in a nested FormRecord. This gives an error.
removeRandomNestedFormControls() {
  const subRecord = this.nestedForm.controls.subRecord;
  Object.keys(subRecord.controls).forEach((key) => {
    if (Math.random() > 0.5) {
      return;
    }
    // The error occurs on the following line
    subRecord.removeControl(key, { emitEvent: false });
  });
}

The error that I get is

No overload matches this call.
Overload 1 of 2, '(this: FormGroup<{ [key: string]: AbstractControl<any, any>; }>, name: string, options?: { emitEvent?: boolean | undefined; } | undefined): void', gave the following error.
The 'this' context of type 'FormGroup<{ [key: string]: FormControl<boolean>; }>' is not assignable to method's 'this' of type 'FormGroup<{ [key: string]: AbstractControl<any, any>; }>'.
Type '{ [key: string]: AbstractControl<any, any>; }' is not assignable to type '{ [key: string]: FormControl<boolean>; }'.
'string' index signatures are incompatible.
Type 'AbstractControl<any, any>' is missing the following properties from type 'FormControl<boolean>': defaultValue, registerOnChange, registerOnDisabledChange
Overload 2 of 2, '(name: never, options?: { emitEvent?: boolean | undefined; } | undefined): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.

The interesting things I see are that I can use addControl() without issues. Another interesting thing (perhaps what causes this error) is that formBuilder seems to change the type of the nested FormRecord to FormGroup. Meaning that subRecord has the type FormRecord<FormControl<boolean>>, whereas the whole nestedForm has the type

FormGroup<{
    field1: FormControl<string>;
    subRecord: FormGroup<{ // <-- not FormRecord?? 
        [key: string]: FormControl<boolean>;     
    }>;
}>

Is this a bug in Angular Forms, or am I missing something obvious? How is this supposed to be done?


Solution

  • There is an inference issue/bug with FormBuild.group with doesn't return subRecord as FormRecord

      nestedForm = this.formBuilder.group({
        field1: [``],
        subRecord: this.formBuilder.record<boolean>({}),
      });
    

    It's inferred as

    FormGroup<{
        field1: FormControl<string>;
        subRecord: FormGroup<{
            [key: string]: FormControl<boolean>;
        }>;
    }>
    

    The easiest fix is to specify the return type of subRecord

    const subRecord: FormRecord<FormControl<boolean>> = this.nestedForm.controls.subRecord;