Search code examples
angulartypescriptangular-formsangular-controlvalueaccessor

Angular 14: deleting items from FormArray clears the values of remaining items


I created a form using custom inputs and ControlValueAccessor which you can see here. After finally getting it to work I noticed whenever I delete an item from a FormArrayit clears the values of the other FormGroups still present in the array. If you run the demo and click on the add-media-query button a couple times and fill one out then delete one of the others you'll see what I mean. The code bellow is an example of how I have everything set up.

The form

SvgForm : FormGroup<SvgForm> = new FormGroup<SvgForm>({
    title: new FormControl<string>(''),
    graphicId: new FormControl<string>(''),
    svgInput : new FormControl<string>(''),
    viewBox : new FormGroup<ViewBoxParams>({
      x: new FormControl<string>(''),
      y: new FormControl<string>(''),
      width: new FormControl<string>(''),
      height: new FormControl<string>('')
    }),
    styling: new FormGroup<StylingParams>({
      globalStyles: new FormControl<string>(''),
      mediaQueries: new FormArray<FormGroup<MediaQueryParams>>([])
    })
  });

Inside the template I pass the styling FormGroup into a custom component called styling-input through an @Input() like so

<styling-input [StylingForm]="SvgForm.controls.styling"></styling-input>

This is how the styling-input is set up.

styling-input.component.ts

@Component({
  selector: 'styling-input',
  templateUrl: './styling-input.component.html',
  styleUrls: ['./styling-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: StylingInputComponent,
      multi: true
    }
  ]
})
export class StylingInputComponent implements ControlValueAccessor{

  @Input() StylingForm!: FormGroup<StylingParams>;
  
  get MediaQueryList() {
    return this.StylingForm.get('mediaQueries') as FormArray<FormGroup<MediaQueryParams>>;
  }

  writeValue(value: any){ if(value){ this.StylingForm.setValue(value); } }

  registerOnChange(fn: any){ this.StylingForm.valueChanges.subscribe(fn); }

  registerOnTouched(onTouched: Function){}

  private createMediaQueryGroup(): FormGroup<MediaQueryParams> {

    return new FormGroup<MediaQueryParams>({
      selectorParams: new FormGroup<MediaSelectorParams>({
        mediaType: new FormControl<MediaTypeParams>('all'),
        expressions: new FormArray<FormGroup<MediaExpressionGroupParams>>([]),
      }),
      rules: new FormControl<string>('')
    });
  }

  public addMediaQuery():void{
    this.MediaQueryList.push(this.createMediaQueryGroup());
  }

  public removeMediaQuery(item: number): void{
    this.MediaQueryList.removeAt(item);
  }

}

Then inside the template I iterate over the MediaQueryList getter like this

<article formArrayName="mediaQueries">

  <media-query-input *ngFor="let a of MediaQueryList.controls; let i = index"
    [MediaQueryForm]="a"
    [attr.GroupId]="i"
    (RemoveGroup)="removeMediaQuery($any($event))"
  ></media-query-input>

</article>

The property MediaQueryForm is an @Input() that I pass the FormGroup into and the i variable is passed back up through the RemoveGroup Output once a delete button is pushed and you can see in the code above the removeMediaQuery() function uses removeAt() on the MediaQueryList.

I haven't really tried anything different as this is the suggested way to remove an element from a FormArray, however due to me using ControlValueAccessor I'm guessing there may be some other things going on under the hood that I'm not aware of. Does anybody know why this happens and how to fix it?


Solution

  • You are not passing GroupId input value to the media-query-input component.

    Instead of setting attribute value pass input using without attr prefix.

    [attr.GroupId]="i" ===> [GroupId]="i"
    

    styling.input.component

     <media-query-input *ngFor="let a of MediaQueryList.controls; let i = index"
            [MediaQueryForm]="a"
            [GroupId]="i"
            (RemoveGroup)="removeMediaQuery($any($event))" >
     </media-query-input>
    

    Forked Example