50

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>
Thatkookooguy
  • 6,669
  • 1
  • 29
  • 54
JR90
  • 527
  • 1
  • 6
  • 8

3 Answers3

102

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;
}
AT82
  • 71,416
  • 24
  • 140
  • 167
  • 4
    Great answer as usual – yurzui Jan 30 '18 at 18:17
  • Thank you Alex, this doesn't completely solve my issue though. I've determined what the issue is, but not how to solve it... Assuming that the form should be pre-populated with data... I'm not actually sure how to set the form control properly. – JR90 Jan 31 '18 at 03:03
  • @yurzui Thank you, you are very kind Mr, and the same back at you! ;) – AT82 Jan 31 '18 at 11:12
  • Thank you @Alex, I used this as a template to figure out how to integrate this when the form is using ngOnChanges instead of ngOnInit. I've posted my findings to my original question as an edit.I'm marking your response as the answer since you helped me over the hurdle :) – JR90 Feb 02 '18 at 03:49
  • if the companies are bind from a array for loop.. how to handle that scenario? and the companies are check boxes – Anoop Babu May 08 '18 at 12:07
  • How to know in which form I am? – POV Jul 31 '18 at 22:39
  • @AJT82 sorry for asking a question in comment box because i am ban to ask questions on stackoverflow my problem is related to this answer so please take a look here- https://stackblitz.com/edit/angular-66qunm – Rahul Verma Nov 11 '19 at 11:40
  • @RahulVerma, your stackblitz with static data: https://stackblitz.com/edit/angular-8jqjnr?file=src/app/hotel-edit/hotel-edit.component.ts You were trying to set an formarray as `setValue` for your formarray, but a formarray takes groups of formcontrols or formgroups, not an already built formarray, so I push formgrouups to the formarray in your form directly. – AT82 Nov 11 '19 at 14:25
  • @RahulVerma, OR... if you don't want for some reason to push to the formarray directly like in above comment, you can use `setControl()`, which **replaces** an existing formcontrol/formgroup/formarray: https://stackblitz.com/edit/angular-rrgw8r?file=src/app/hotel-edit/hotel-edit.component.ts – AT82 Nov 11 '19 at 14:33
  • @AJT82 i followed your steps its working fine but there is a small problem please take a look https://stackblitz.com/edit/angular-66qunm?file=src%2Fapp%2Fhotel-edit%2Fhotel-edit.component.ts – Rahul Verma Nov 11 '19 at 21:06
  • @RahulVerma I don't know how you intend to add these, here using a button. Basically just push a formgroup to the formarray: https://stackblitz.com/edit/angular-ajvkzk?file=src/app/hotel-edit/hotel-edit.component.html – AT82 Nov 12 '19 at 10:03
  • 1
    Seems to work, but get an error in my IDEA "Identifier 'controls' is not defined. 'AbstractControl' does not contain such a member" not sure if it will pass linter during the build. – Experimenter Jul 01 '21 at 16:26
  • 1
    @Experimenter Thank you for pointing these things out! A lot has changed since 2018, so this answer needed an update! Both the answer and demo has been updated, I did a lot of changes, so sift through the code. The typechecking for the nested array is tricky as you cannot use a getter like for the outer formarray (companies), a workaround is needed in that case... escape the typechecking or calling a function. You need to make a choice how you want to do that. I have escaped the typechecking in my answer, since I don't like the idea of calling a function in template... – AT82 Jul 02 '21 at 06:26
-2

Here is what I did I used angularflexlayout and angular material, you can yous any library, just wanted to show you the functionality

     <form [formGroup]="editForm" novalidate fxLayout="column"
        autocomplete="off">

    <div fxLayout="row wrap" fxLayoutGap="2em">
      <mat-form-field [fxFlex]="30">
        <mat-label>Name</mat-label>
        <input matInput formControlName="name" width="800px"/>
      </mat-form-field>

    </div>
    <div fxLayout="column" fxLayoutGap="3em">
      <div fxLayout="row wrap" fxFlex="40" fxLayoutGap="2em">
        <div formArrayName="phones" fxFlex="50" fxLayoutGap="8px" *ngFor="let phone of Phones.controls;let i= index">
          <mat-form-field fxFlex="100" [formGroupName]="i">
            <mat-label>Phone</mat-label>
            <input matInput formControlName="phone"/>
          </mat-form-field>
        </div>
        <button type="button" mat-stroked-button color="primary" (click)="addPhone()">add</button>
      </div>
      <div fxLayout="row wrap" fxFlex="40" fxLayoutGap="2em">
        <div formArrayName="emails" fxFlex="50" fxLayoutGap="8px" *ngFor="let email of Emails.controls;let i= index">
          <mat-form-field fxFlex="100" [formGroupName]="i">
            <mat-label>Email</mat-label>
            <input matInput formControlName="email"/>
          </mat-form-field>
        </div>
        <button type="button" mat-stroked-button color="primary" (click)="addEmail()">add</button>
      </div>
    </div>

    <div class="mr-2" fxLayoutAlign="end" mat-dialog-actions>
      <button type="button" (click)="cancelDialog()" mat-button
              mat-dialog-close>Cancel
      </button>

      <button type="button" (click)="onSubmit()"
              mat-raised-button
              color="primary">
        Submit
      </button>
    </div>

  </form>

then angular controller

  editForm: FormGroup;
  phones: FormArray;
  emails: FormArray;

  createForm() {
    this.editForm = this.fb.group({
      name: [''],
        phones: this.fb.array([this.createPhone()]),
      emails: this.fb.array([this.createEmail()]),
     });

  }

  get Phones() {
    return this.editForm.get('phones') as FormArray;
  }

  get Emails() {
    return this.editForm.get('emails') as FormArray;
  }

 createPhone() {
    return this.fb.group(({
      phone: '',
    }));
  }

  createEmail() {
    return this.fb.group(({
      email: ''
    }));
  }


  addPhone(): void {
    this.phones = this.editListingForm.get('phones') as FormArray;
    this.phones.push(this.createPhone());
  }

  addEmail(): void {
    this.emails = this.editListingForm.get('emails') as FormArray;
    this.emails.push(this.createEmail());
  }
Eyayu Tefera
  • 771
  • 9
  • 9
-2

                    <tr formArrayName="entries"
                        *ngFor="let field of entriesGroup.get('entries').controls; let ind1 = index;">
                        <td [formGroupName]="ind1">
                            <input type="text" disabled formControlName="date1" name="date1"
                                class="form-control">
                        </td>


                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" formControlName="startingLocation" name="startingLocation"
                                    class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" formControlName="endingLocation" name="endingLocation"
                                    class="form-control">
                            </div>
                        </td>


                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" formControlName="odometerReadStartingPoint"
                                    name="odometerReadStartingPoint" class="form-control"
                                    [ngModel]="ind1!=0?entriesGroup.get('entries').controls[ind1-1].get('odometerReadingDetails').get('odometerReadEndingPoint').value:field.get('odometerReadingDetails').get('odometerReadStartingPoint').value">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" formControlName="odometerReadEndingPoint"
                                    name="odometerReadEndingPoint" class="form-control"
                                    [ngModel]="ind1<entriesGroup.get('entries').controls.length-1?entriesGroup.get('entries').controls[ind1+1].get('odometerReadingDetails').get('odometerReadStartingPoint').value:field.get('odometerReadingDetails').get('odometerReadEndingPoint').value">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" disabled formControlName="odometerReadForOfficial"
                                    name="odometerReadForOfficial" class="form-control"
                                    [ngModel]="field.get('odometerReadingDetails').get('totalKilometersCovered').value-field.get('odometerReadingDetails').get('odometerReadForPersonal').value">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" formControlName="odometerReadForPersonal"
                                    name="odometerReadForPersonal" class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <input type="text" disabled formControlName="totalKilometersCovered"
                                    name="totalKilometersCovered" class="form-control"
                                    [ngModel]="field.get('odometerReadingDetails').get('odometerReadEndingPoint').value!=0?field.get('odometerReadingDetails').get('odometerReadEndingPoint').value-field.get('odometerReadingDetails').get('odometerReadStartingPoint').value:0">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideHeaders">
                            <div formGroupName="odometerReadingDetails">
                                <textarea rows="2" cols="20" type="text" formControlName="particularTravel"
                                    name="particularTravel" class="form-control">

                         </textarea>
                            </div>
                        </td>



                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="fuelFilled" name="fuelFilled"
                                    class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="costPerLiter" name="costPerLiter"
                                    class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" disabled formControlName="costOfTheFuel" name="costOfTheFuel"
                                    class="form-control"
                                    [ngModel]="field.get('fuelDetails').get('costPerLiter').value * field.get('fuelDetails').get('fuelFilled').value">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" disabled formControlName="fuelConsumption"
                                    name="fuelConsumption" class="form-control"
                                    [ngModel]="field.get('fuelDetails').get('costOfTheFuel').value!=0?(ind1!=0?(entriesGroup.get('entries').controls[ind1-1].get('fuelDetails').get('fuelConsumption').value + field.get('fuelDetails').get('costOfTheFuel').value):field.get('fuelDetails').get('costOfTheFuel').value):0">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="NoNeed">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="requestId" name="requestId"
                                    class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="couponCode" name="couponCode"
                                    class="form-control">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="hideDate">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="couponDate" name="couponDate"
                                    placeholder="date1,date2,..." class="form-control">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="fuelHide">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="couponNumber" name="couponNumber"
                                    placeholder="coupon1,coupon2,..." class="form-control">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="hideDate">
                            <div formGroupName="fuelDetails">
                                <input type="text" formControlName="couponsAmount" name="couponsAmount"
                                    class="form-control">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="totalKmsCoveredBeforeRepair"
                                    name="totalKmsCoveredBeforeRepair" class="form-control" readonly
                                    [ngModel]="field.get('odometerReadingDetails').get('totalKilometersCovered').value">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="sparesCost" name="sparesCost"
                                    class="form-control">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="labourCost" name="labourCost"
                                    class="form-control">
                            </div>
                        </td>


                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="vehicleRepaireCost"
                                    name="vehicleRepaireCost" class="form-control"
                                    [ngModel]="field.get('vehicleRepairDetails').get('sparesCost').value*1 + field.get('vehicleRepairDetails').get('labourCost').value*1">
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">

                                <textarea rows="2" cols="20" type="text" formControlName="particularsOfRepairs"
                                    name="particularsOfRepairs" class="form-control">

                           </textarea>
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="workShop">
                            <div formGroupName="vehicleRepairDetails">
                                <select formControlName="workshopId" class="form-control"
                                    (click)="workshopDetails1($event,ind1)">

                                    <option *ngFor="let i of workshopdata"
                                        [selected]="i.workShopId==field.get('vehicleRepairDetails').get('workshopId').value">
                                        {{i.workShopId}}</option>
                                    <option value="others">OTHERS</option>
                                </select>
                            </div>
                        </td>


                        <td [formGroupName]="ind1" *ngIf="workShop">
                            <div formGroupName="vehicleRepairDetails">
                                <textarea rows="2" cols="50" type="text" formControlName="workshopAddress"
                                    name="workshopAddress" class="form-control">
           </textarea>
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="workShop">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="workshopContactNumber"
                                    name="workshopContactNumber" class="form-control">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="workShop">
                            <div formGroupName="vehicleRepairDetails">

                                <textarea rows="2" cols="50" type="text"
                                    formControlName="workshopAccountDetails" name="workshopAccountDetails"
                                    class="form-control">
           </textarea>
                            </div>
                        </td>
                        <td [formGroupName]="ind1" *ngIf="repaireHide">
                            <div formGroupName="vehicleRepairDetails">
                                <input type="text" formControlName="requestId" name="requestId"
                                    class="form-control">
                            </div>
                        </td>




                        <td [formGroupName]="ind1" *ngIf="driverHide">
                            <div formGroupName="driverChargesDetails">
                                <select formControlName="categoryType" class="form-control"
                                    (click)="onCatType1($event,ind1)">
                                    <option>Choose</option>
                                    <option *ngFor="let i of driverChargesLimit"
                                        [selected]="i.categoryType==field.get('driverChargesDetails').get('categoryType').value">
                                        {{i.categoryType}}</option>

                                </select>
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="driverHide">
                            <div formGroupName="driverChargesDetails">
                                <input type="text" formControlName="amount" name="amount" class="form-control"
                                    disabled>
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="driverHide">
                            <div formGroupName="driverChargesDetails">
                                <input type="time" formControlName="startingTime" name="startingTime"
                                    class="form-control" (change)="onStartTime1($event,ind1)">
                            </div>
                        </td>

                        <td [formGroupName]="ind1" *ngIf="driverHide">
                            <div formGroupName="driverChargesDetails">
                                <input type="time" formControlName="endingTime"
                                    (change)="onEndTime1($event,ind1)" name="endingTime" class="form-control">
                            </div>
                        </td>

                    </tr>

                </tbody>
            </div>
        </table>
  • 2
    If it contains 8 objects data it's repeating fine.But if it contains 10 and above its not repeating how solve...? – user12635584 Jan 01 '20 at 08:22