DEV Community

loading...

Using PatchValue on FormArrays in Angular 7

crazedvic profile image Vic Rubba ・3 min read

February 14th, 2019

So I created fancy form with nested FormArrays, and was able to very easily persist that data as a document in Firestore. Specifically I was working on a weekly timesheet view, and being able to see it save exactly as it was in the angular form was quite awesome. Then it came time to reload that data into the same form. And that's when the pain began...

I have actually found Angular's reactive forms pretty terrible - they are easy to get up and running with little to no effort, but then to actually use them in production code suddenly demands a fairly steep learning curve. In Angular 7 it is said that two way binding with ngModel is no longer possible, so this only adds to the confusion. But enough ranting, let's get started.

The structure of the form and the data is as follows

Team 
  Lines
    Line
      Players
        Player

So a team can have many lines and each line can have many players. Keeping it simple. There's no limit or minimums to the number of lines or players.

The challenge we face is that when the form first renders, it does not know how many lines or how many players it will need to render out form elements for. We need to look at the data, parse through the data and build our form so that it has all the elements we need. Then if we've done things correctly, we can just patchValue the form formgroup and all the values should appear.

First let's look at some sample seed data:

const seedData =  {
  "team_name": "The Team",
  "lines": [
    {
      "name": "line 1",
      "players": [
        {
          "first_name": "1a",
          "last_name": ""
        },
        {
          "first_name": "2a",
          "last_name": ""
        },
        {
          "first_name": "3a",
          "last_name": ""
        }
      ]
    },
    {
      "name": "line 2",
      "players": [
        {
          "first_name": "1b",
          "last_name": ""
        },
        {
          "first_name": "2b",
          "last_name": ""
        },
        {
          "first_name": "3b",
          "last_name": ""
        }
      ]
    },
    {
      "name": "line 3",
      "players": [
        {
          "first_name": "1c",
          "last_name": ""
        },
        {
          "first_name": "2c",
          "last_name": ""
        },
        {
          "first_name": "3c",
          "last_name": ""
        }
      ]
    }
  ]
}

And in ngOnInit I first set the form up as follows:

ngOnInit() {
    this.dataModel = Object.create(null);

    // created a simple form with no populated formarray
    // this will be handled by the data load instead
    this.cvForm = this._fb.group({
       team_name: ['', [Validators.required]],
       lines: this._fb.array([
         ])      
    });

     //subscribe to value changes on form
     // only way i've found i can manipulate the form data
     // through code and have it update the underlying dataModel
     this.cvForm.valueChanges.subscribe(data=>{
        this.dataModel = data;
     });
  }

Let's look at the code:

loadForm(data){
    // create lines array first
    for (let line = 0; line < data.lines.length; line++){
       const linesFormArray = this.cvForm.get("lines") as FormArray;
       linesFormArray.push(this.line);

       //for each line, now add all the necessary player formgroups
       for (let player=0; player < data.lines[line].players.length; player++){
          const playersFormsArray = linesFormArray.at(line).get("players") as FormArray;
          playersFormsArray.push(this.player);
       }
    }

    // once we setup the form with all the arrays and such, we can just
    // patch the form:
    this.cvForm.patchValue(data);
  }

As you can see I examine the data, loop through the child arrays in the data, and build out my form. It will create all the form elements it needs, here's the get functions it's calling and pushing into the formArrays:

get player():FormGroup{
    return this._fb.group({
        first_name: '',
        last_name: ''
    });
}

get line():FormGroup{
    return this._fb.group({
        name: '',
        players: this._fb.array([
         ]),
    })
}

Finally, on save the last trick is to reset the form, in other words, clear all the controls and start fresh, so that subsequent loads don't keep adding on top of the existing formArrays. From what I've read, actually using removeAt of all the formArrays and their children would break my subscription. So the trick is to do as follows:

saveForm(){
    //save form values to storeData and reset the form
    this.storeData = this.cvForm.getRawValue();
    this.cvForm.reset();
    // IMPORTANT: reset will not remove form elements only clear them
    // So we need to clear the FormArrays as well 
    // without losing our subscription! 
    // this seems to work fine:
     (this.cvForm.get("lines") as FormArray)['controls'].splice(0);
  }

I am hoping this will help someone else get to where i am today just a little faster. You can find the working code here:

https://stackblitz.com/edit/angular-patch-value-deeply-nested-component-utk3qv

Discussion (3)

pic
Editor guide
Collapse
da13harris profile image
Dale Harris

As of Angular 8, there is now a FormArray.clear() method that removes all of the controls from a FormArray. This should simplify the saveForm() method a bit.

Collapse
wianlloyd profile image
Wian Lloyd

🤠🙏🍞

Collapse
boehner profile image
Boehner

I see that Angular 7 has 2-way binding
angular.io/api/forms/NgModel#descr...