2
votes

I've encountered a strange bug with Angular reactive forms. I am using a FormArray to create a form where fields can be added or deleted. I also want to be able to reset the form whereby the values and number of form inputs goes back to the original amount. I'm currently able to instantiate the form, add fields and delete them fine but when I press reset the function I've created which firsts empties the FormArray and then recreates the fields using the same process as I used to set up the form initially, the value aren't be displayed properly. I'm not sure why this is happening, perhaps it's to do with the formControlNames used to bind the form in the html?

Does anyone know what's causing the issue or what's the proper way to reset the form values?

I've created a stackblitz here: https://stackblitz.com/edit/angular-reactive-formarray-bug

Here's my component code.

import {
  Component, ElementRef
} from '@angular/core';
import { FormBuilder, FormGroup, FormArray, FormControl } from '@angular/forms';

import { ContentTemplate, ContentTemplateEntry, NewContentEntry } from './models';

import {Observable, Subscription, of} from 'rxjs';

import data from './template-data.json';

@Component({
  selector: 'material-app',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  public templateForm: FormGroup;

  public contentTemplate$: Observable<ContentTemplate>;
  public activeTemplate: ContentTemplate;
  public templateSub: Subscription;
  public entries: ContentTemplateEntry[];

  get templateEntries(): FormArray {
    return <FormArray>this.templateForm.get('entries');
  }

  constructor(
    private fb: FormBuilder
  ) {
    this.contentTemplate$ = of(data)
  }

  ngOnInit(): void {
    this.templateSub = this.contentTemplate$.subscribe((template: ContentTemplate) => {
      this.activeTemplate = {...template};
      this.entries = [...template.entries];
      this.templateForm = this.fb.group({
        entries: this.fb.array([])
      });
      this._processEntries(this.entries);
    });
  }

  ngOnDestroy(): void {
    this.templateSub.unsubscribe();
  }

  private _buildEntry(entry: ContentTemplateEntry) {
    const g = this.fb.group({
      id: {value: entry.id},
      title: {value: entry.title, disabled: entry.isRequired},
      orderNumber: {value: entry.orderNumber},
      type: {value: entry.type}
    });
    return g;
  }

  private _processEntries(entries: ContentTemplateEntry[])  {
    entries.forEach((e, i) => {
      this.templateEntries.push(this._buildEntry(e));
    });
  }

  private _getOrderNumber(): number {
    return this.templateEntries.length + 1;
  }

  private _removeItemsFromEntries(): void {
    while (this.templateEntries.length > 0) {
      this.templateEntries.removeAt(0);
    }
  }

  // reinstantiate using the same approach as before
  public resetForm() {
    this._removeItemsFromEntries();
    this._processEntries(this.entries);
  }

  public removeEntry(id: number) {
    this.templateEntries.removeAt(id);
  }

  public addEntry() {
    this.templateEntries.push(
      this._buildEntry(new NewContentEntry({
        orderNumber: this._getOrderNumber()
      }))
    );
  }

  public save() {
    console.log('save triggered');
  }
}
4

4 Answers

2
votes

Your error is caused by type="reset" on the Reset button. Removing this attribute or replacing with type="button" will resolve the problem.

The proper way of resetting a form is by calling the reset() method on your templateForm property.

Resets the FormGroup, marks all descendants are marked pristine and untouched, and the value of all descendants to null.

Official FormGroup documentation

1
votes

I think this problem with async operations in reactive forms. setTimeout fix this problem, but i think is not the best way.

public resetForm() {
  setTimeout(() => this._processEntries(this.entries));
}

Also you can refactor your processEntries method.

private _processEntries(entries: ContentTemplateEntry[]) {
  const resEntries = entries.map(e => this._buildEntry(e));
  this.templateForm.setControl('entries', this.fb.array(resEntries));
}

And _removeItemsFromEntries don`t need more.

example here https://stackblitz.com/edit/angular-reactive-formarray-bug-pvj4cm

0
votes

I used once FormArray and I did the following:

const control = <FormArray>this.templateForm.controls['entries'];
for(let i = control.length-1; i >= 0; i--) {
  control.removeAt(i)
}
0
votes

Better this way:

const control = <FormArray>this.templateForm.controls['entries'];
while (control.length > 0) {
  control.removeAt(0)
}