Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Reactive Forms with nested Form Arrays

I'm new to Angular 2 and decided the best way to learn would be to go through the official Angular guides.

I went through the Reactive Forms Guide https://angular.io/guide/reactive-forms

demo link: https://stackblitz.com/angular/jammvmbrpxle

While the content was overall pretty good, I'm stuck on how I would go about implementing a more complex Form. In the given example, each Hero has the potential for many addresses. An address itself is a flat object.

What if Addresses had additional information such as the color and type of rooms located at the address.

export class Address {     street = '';     city   = '';     state  = '';     zip    = '';     rooms = Room[]; }  export class Room {      type = ''; } 

so that the form model would look like this...

createForm() { this.heroForm = this.fb.group({   name: '',   secretLairs: this.fb.array([       this.fb.group({           street: '',           city: '',           state: '',           zip: '',           rooms: this.fb.array([               this.fb.group({                  type: ''           })]),       })]),   power: '',   sidekick: '' }); 

}

EDIT - Finalized Code that works with ngOnChanges

hero-detail.component.ts

createForm() {     this.heroForm = this.fb.group({       name: '',       secretLairs: this.fb.array([         this.fb.group({           street: '',           city: '',           state: '',           zip: '',           rooms: this.fb.array([             this.fb.group({               type: ''             })           ])         })       ]),       power: '',       sidekick: ''     });   }    ngOnChanges() {     this.heroForm.reset({       name: this.hero.name,     });     this.setAddresses(this.hero.addresses);   }    setAddresses(addresses: Address[]) {     let control = this.fb.array([]);     addresses.forEach(x => {       control.push(this.fb.group({         street: x.street,         city: x.city,         state: x.state,         zip: x.zip,         rooms: this.setRooms(x) }))     })     this.heroForm.setControl('secretLairs', control);   }    setRooms(x) {     let arr = new FormArray([])     x.rooms.forEach(y => {       arr.push(this.fb.group({          type: y.type        }))     })     return arr;   } 

hero-detail.component.html (the nested form array portion)

<div formArrayName="secretLairs" class="well well-lg">   <div *ngFor="let address of heroForm.get('secretLairs').controls; let i=index" [formGroupName]="i" >     <!-- The repeated address template -->     <h4>Address #{{i + 1}}</h4>     <div style="margin-left: 1em;">       <div class="form-group">         <label class="center-block">Street:           <input class="form-control" formControlName="street">         </label>       </div>       <div class="form-group">         <label class="center-block">City:           <input class="form-control" formControlName="city">         </label>       </div>       <div class="form-group">         <label class="center-block">State:           <select class="form-control" formControlName="state">             <option *ngFor="let state of states" [value]="state">{{state}}</option>           </select>         </label>       </div>       <div class="form-group">         <label class="center-block">Zip Code:           <input class="form-control" formControlName="zip">         </label>       </div>     </div>     <br>     <!-- End of the repeated address template -->     <div formArrayName="rooms" class="well well-lg">       <div *ngFor="let room of address.get('rooms').controls; let j=index" [formGroupName]="j" >           <h4>Room #{{j + 1}}</h4>           <div class="form-group">             <label class="center-block">Type:               <input class="form-control" formControlName="type">             </label>           </div>       </div>     </div>   </div>   <button (click)="addLair()" type="button">Add a Secret Lair</button> </div> 
like image 292
JR90 Avatar asked Jan 25 '18 05:01

JR90


People also ask

Can we have nested forms in angular?

Nested Forms will allow us to create the array of objects for a single field and this hierarchy goes on. (i.e) A single object can have a number of objects inside of it, and we can achieve it in Reactive forms through the concept of “Form Array”.

Should I use template driven forms or reactive forms?

Template Driven Forms are based only on template directives, while Reactive forms are defined programmatically at the level of the component class. Reactive Forms are a better default choice for new applications, as they are more powerful and easier to use.


1 Answers

EDIT: 2021 As the typechecking has become more strict (good!) we need to do some changes. Typing the nested formarray cannot be done using a getter. You can use a function instead, but I don't like that idea, as it is called on each change detection. Instead I am working around the typechecking and using ['controls'] instead. If you do want stronger typing for nested array (projects) use a function, but remember the fact that it is called on each change detection... So here is the updated code:

It's not very much different to have a nested formarray. Basically you just duplicate the code you have... with nested array :) So here's a sample:

myForm: FormGroup;  constructor(private fb: FormBuilder) {   this.myForm = this.fb.group({     // you can also set initial formgroup inside if you like     companies: this.fb.array([])   }) }  // getter for easier access get companiesFormArr(): FormArray {   return this.myForm.get('companies') as FormArray; }  addNewCompany() {   this.companiesFormArr.push(     this.fb.group({       company: [''],       projects: this.fb.array([])     })   ); }  deleteCompany(index: number) {   this.companiesFormArr.removeAt(index); } 

So that is the add and delete for the outermost form array, so adding and removing formgroups to the nested form array is just duplicating the code. Where from the template we pass the current formgroup to which array you want to add (in this case) a new project/delete a project.

addNewProject(control) {   control.push(     this.fb.group({       projectName: ['']   })) }  deleteProject(control, index) {   control.removeAt(index) } 

And the template in the same manner, you iterate your outer formarray, and then inside that iterate your inner form array:

<form [formGroup]="myForm">   <div formArrayName="companies">     <div *ngFor="let comp of companiesFormArr.controls; let i=index">     <h3>COMPANY {{i+1}}: </h3>     <div [formGroupName]="i">       <input formControlName="company" />       <button (click)="deleteCompany(i)">          Delete Company       </button>       <div formArrayName="projects">         <!-- Here I has worked around the typechecking,               if you want stronger typechecking, call a function.               Remember: function called on each change detection! -->         <div *ngFor="let project of comp.get('projects')['controls']; let j=index">           <h4>PROJECT {{j+1}}</h4>           <div [formGroupName]="j">             <input formControlName="projectName" />             <button (click)="deleteProject(comp.get('projects'), j)">               Delete Project             </button>           </div>         </div>         <button (click)="addNewProject(comp.get('projects'))">           Add new Project         </button>       </div>     </div>   </div> </div> 

DEMO

EDIT:

To set values to your form once you have data, you can call the following methods that will iterate your data and set the values to your form. In this case data looks like:

data = {   companies: [     {       company: "example comany",       projects: [         {           projectName: "example project",         }       ]     }   ] } 

We call setCompanies to set values to our form:

setCompanies() {   this.data.companies.forEach(x => {     this.companiesFormArr.push(this.fb.group({        company: x.company,        projects: this.setProjects(x) }))   }) }  setProjects(x) {   let arr = new FormArray([])   x.projects.forEach(y => {     arr.push(this.fb.group({        projectName: y.projectName      }))   })   return arr; } 
like image 66
AT82 Avatar answered Sep 19 '22 00:09

AT82