2

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 Answers 4

2

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

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! This does appear to have been the solution! This fixes my problem and means I don't need to wrap the _processEntries function in a setTimeout. Thanks again!
1

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

2 Comments

Thanks for your help Kliment. I had a work around before where I was just dispatching an action to recreate the original object in the store which then filtered down to the component. This is definitely cleaner than that.
Hello again Kliment, @Andreas found the problem with my form. The reset type attribute that was wrongly applied to my reset button meant that the form was being altered before I reset it. I guess the setTimeout solution must have worked by pushing the processEntries function to the back of the event cycle.
0

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)
}

Comments

0

Better this way:

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

1 Comment

Welcome to StackOverflow! Please provide a specific answer backed with references. If you have a suggestion or need clarification leave a comment. We have guidelines on How to write a good answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.