23

I have a very complex and large data and I have to build a Reactive form around it in a component.

I've developed the form. But when I type something in one of the input fields to edit the populated data, it responds extremely slowly in updating the value of that field.

I tried using updateOn:'blur' and 'submit' but without any luck.

My question is, what is best practice to handle the form with large data?

Update: This is my StackBlitz.

Note: I've created a very minimal version of my actual implementation and I have performance issues in the Reactive Form.

SiddAjmera
  • 38,129
  • 5
  • 72
  • 110
dam
  • 256
  • 1
  • 3
  • 12
  • [Related / Possible dubplicate](https://stackoverflow.com/questions/50859056/best-practice-to-handle-large-form-in-angular-2) - Although I don't like the answer to this question – Jonas Praem Feb 05 '19 at 10:21
  • 6
    That would really depend on how you've created the form and how you're binding to it in your template. Please consider creating a Sample StackBlitz replicating the issue and sharing it here. It would be really helpful. – SiddAjmera Feb 05 '19 at 10:22
  • "i try to use updateOn:'blur' and 'submit' it". Sorry, I don't see that – Eliseo Feb 05 '19 at 10:28
  • @Eliseo i try to use in my project but it does not work so , i wll remove it – dam Feb 05 '19 at 10:39
  • @SiddAjmera thank you ,i will update later – dam Feb 05 '19 at 10:39
  • edited @SiddAjmera – dam Feb 06 '19 at 04:31

1 Answers1

35

So after about a day of playing around with your StackBlitz, here I am with the solution. I think this would significantly improve the performance.

Step 1: Create interfaces for your Data Model

Doing that would significantly make the code cleaner and more readable. It would also make the code more manageable and easy to work with. So here we go with the list of interfaces for your specific scenario:

export interface Hotel {
  id: string;
  currencyId: string;
  hotelYearId: string;
  priceTaxTypeId: string;
  code: string;
  name: string;
  createBy: string;
  createDate: string;
  lastUpdateBy: string;
  lastUpdateDate: string;
  remark: string;
  internalRemark: string;
  roomTypes: RoomType[];
}

export interface RoomType {
  chk: boolean;
  roomTypeId: string;
  mealTypes: MealType[];
}

export interface MealType {
  chk: boolean;
  mealTypeId: string;
  marketGroups: MarketGroup[];
}

export interface MarketGroup {
  chk: boolean;
  markets: Market[];
  rateSegments: RateSegment[];
}

export interface Market {
  marketId: string;
}

export interface RateSegment {
  chk: boolean;
  rateSegmentId: string;
  hotelSeasons: HotelSeason[];
}

export interface HotelSeason {
  chk: boolean;
  hotelSeasonId: string;
  rates: Rate[];
}

export interface Rate {
  rateCodeId: string;
  cancellationPolicyId: string;
  dayFlag: string;
  singlePrice: string;
  doublePrice: string;
  xbedPrice: string;
  xbedChildPrice: string;
  bfPrice: string;
  bfChildPrice: string;
  unitMonth: string;
  unitDay: string;
  minStay: number;
}

Step 2: Change the way you're creating the form

The way you're creating the form is extremely noisy. There's a clear way of doing that. And since you're already creating the form in the service, I suggest you keep the task of creating the form to the service itself and keep your component free from any such task. So your service can be refactored like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators } from '@angular/forms';
import { map } from 'rxjs/operators';

import { Hotel, RoomType, MealType, MarketGroup, Market, RateSegment, HotelSeason, Rate } from './hotel.model';

@Injectable()
export class UtilService {

  constructor(
    private readonly fb: FormBuilder,
    private readonly http: HttpClient
  ) { }

  getHotelForm() {
    return this.getHotel().pipe(
      map((apiResponse: any) => this.fb.group({
        id: [apiResponse.id, Validators.required],
        currencyId: [apiResponse.currencyId, Validators.required],
        hotelYearId: [apiResponse.hotelYearId, Validators.required],
        priceTaxTypeId: [apiResponse.priceTaxTypeId, Validators.required],
        code: [apiResponse.code, Validators.required],
        name: [apiResponse.name, Validators.required],
        createBy: [apiResponse.createBy, Validators.required],
        createDate: [apiResponse.createDate, Validators.required],
        lastUpdateBy: [apiResponse.lastUpdateBy, Validators.required],
        lastUpdateDate: [apiResponse.lastUpdateDate, Validators.required],
        remark: [apiResponse.remark, Validators.required],
        internalRemark: [apiResponse.internalRemark, Validators.required],
        roomTypes: this.fb.array(apiResponse.roomTypes.map(roomType => this.generateRoomTypeForm(roomType)))
      }))
    );
  }

  private getHotel() {
    return this.http.get('/assets/hotel.json');
  }

  private generateRoomTypeForm(roomType: RoomType) {

    const roomTypeForm = this.fb.group({
      chk: [roomType.chk, Validators.required],
      roomTypeId: [roomType.roomTypeId, Validators.required],
      mealTypes: this.fb.array(roomType.mealTypes.map(mealType => this.generateMealTypeForm(mealType)))
    });

    return roomTypeForm;
  }

  private generateMealTypeForm(mealType: MealType) {

    const mealTypeForm = this.fb.group({
      chk: [mealType.chk, Validators.required],
      mealTypeId: [mealType.mealTypeId, Validators.required],
      marketGroups: this.fb.array(mealType.marketGroups.map(marketGroup => this.generateMarketGroupForm(marketGroup)))
    });

    return mealTypeForm;
  }

  private generateMarketGroupForm(marketGroup: MarketGroup) {

    const marketGroupForm = this.fb.group({
      chk: [marketGroup.chk, Validators.required],
      markets: this.fb.array(marketGroup.markets.map(market => this.generateMarketForm(market))),
      rateSegments: this.fb.array(marketGroup.rateSegments.map(rateSegment => this.generateRateSegmentForm(rateSegment))),
    });

    return marketGroupForm;
  }

  private generateMarketForm(market: Market) {
    return this.fb.group({
      marketId: [market.marketId, Validators.required]
    });
  }

  private generateRateSegmentForm(rateSegment: RateSegment) {
    const rateSegmentForm = this.fb.group({
      chk: [rateSegment.chk, Validators.required],
      rateSegmentId: [rateSegment.rateSegmentId, Validators.required],
      hotelSeasons: this.fb.array(rateSegment.hotelSeasons.map(hotelSeason => this.generateHotelSeasonForm(hotelSeason)))
    });

    return rateSegmentForm;
  }

  private generateHotelSeasonForm(hotelSeason: HotelSeason) {

    const hotelSeasonForm = this.fb.group({
      chk: [hotelSeason.chk, Validators.required],
      hotelSeasonId: [hotelSeason.hotelSeasonId, Validators.required],
      rates: this.fb.array(hotelSeason.rates.map(rate => this.generateRateForm(rate)))
    });
    return hotelSeasonForm;
  }

  private generateRateForm(rate: Rate) {
    return this.fb.group({
      rateCodeId: [rate.rateCodeId, Validators.required],
      cancellationPolicyId: [rate.cancellationPolicyId, Validators.required],
      dayFlag: [rate.dayFlag, Validators.required],
      singlePrice: [rate.singlePrice, Validators.required],
      doublePrice: [rate.doublePrice, Validators.required],
      xbedPrice: [rate.xbedPrice, Validators.required],
      xbedChildPrice: [rate.xbedChildPrice, Validators.required],
      bfPrice: [rate.bfPrice, Validators.required],
      bfChildPrice: [rate.bfChildPrice, Validators.required],
      unitMonth: [rate.unitMonth, Validators.required],
      unitDay: [rate.unitDay, Validators.required],
      minStay: [rate.minStay, Validators.required]
    });
  }

}

Step 3: Leverage the above service:

Do it to get the Form and get rid of the methods that would return to you the FormArrays in your template. That would make your Component very clean, clear and concise.

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';

import { UtilService } from '../app/util.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class AppComponent {
  
  form$: Observable<FormGroup> = this.util.getHotelForm();

  constructor(private readonly util: UtilService) {
  }

}

Step 4: Refactor your Template:

And this one is THE MOST IMPORTANT. NEVER call getters or methods in deeply nested forms to get the FormArrays. Or rather, in normal forms or inside a data binding syntax in general. Because they will get called in every single change detection cycle and would kill your App's performance.

Please refer to this lightning talk by Tanner Edwards from ng-conf 2018 to know more about it.

So, you can refactor your Component Template like this:

<form 
  *ngIf="form$ | async as form" 
  [formGroup]="form">
    <div 
    formArrayName="roomTypes">
        <div 
      *ngFor="let roomType of form.controls['roomTypes'].controls; let index = index" 
      [formGroupName]="index">
            {{index}}
            <div 
        formArrayName="mealTypes">
                <div 
          *ngFor="let mealType of roomType.controls['mealTypes'].controls; let mealtypeIndex = index"
          [formGroupName]="mealtypeIndex">
                    mealtype {{mealtypeIndex}}
                    <div 
            formArrayName="marketGroups">
                        <div 
              *ngFor="let marketGroup of mealType.controls['marketGroups'].controls; let marketGroupIndex = index" 
              [formGroupName]="marketGroupIndex">
                            marketGroupIndex {{marketGroupIndex}}
                            <div formArrayName="rateSegments">
                                <div 
                  *ngFor="let rateSegment of marketGroup.controls['rateSegments'].controls; let rateSegmentIndex = index"
                  [formGroupName]="rateSegmentIndex">
                                    rateSegmentIndex {{rateSegmentIndex}}
                                    <div formArrayName="hotelSeasons">
                                        <div 
                      class="fifth_border" 
                      *ngFor="let hotelseason of rateSegment.controls['hotelSeasons'].controls; let hotelseasonIndex = index"
                      [formGroupName]="hotelseasonIndex">
                                            hotelseasonIndex {{hotelseasonIndex}}
                                            <div formArrayName="rates">
                                                <div 
                          *ngFor="let rate of hotelseason.controls['rates'].controls; let rateIndex = index"
                          [formGroupName]="rateIndex">
                          <div style="display:flex;flex-flow;row">
                            <div>
                              <p>SGL</p>
                              <input class="input text_right" type="text" formControlName="singlePrice">
                            </div>
                            <div>
                              <p>DLB/TWN</p>
                              <input class="input text_right" type="text"  formControlName="doublePrice">
                            </div>
                            <div>
                              <p>EX-Adult</p>
                              <input class="input text_right" type="text"  formControlName="xbedPrice" >
                            </div>
                            <div>
                              <p>EX-Child</p>
                              <input class="input text_right" type="text"  formControlName="xbedChildPrice">
                            </div>
                            <div>
                              <p>Adult BF</p>
                              <input class="input text_right" type="text"  formControlName="bfPrice">
                            </div>
                            <div>
                              <p>Child BF</p>
                              <input class="input text_right" type="text"  formControlName="bfChildPrice">
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div> 
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- <pre>{{form.value | json}}</pre> -->
</form>

Step 5: Don't stop here

This is not the end. It's just the beginning. You can also abstract the Child Form(the marketGroup FormGroup inside the marketGroups FormArray) into a separate component. And then you can set it's changeDetectionStrategy to OnPush. This would give you an even better performance.

Here's a StackBliz that you can refer to, to have a look at that solution.

Doing all this would significantly improve the performance of the form.

I hope this helps. I'll try to update this answer if I find anything else to improve the performance beyond this limit.


Here's a Working Sample StackBlitz for your ref.


Here's a Detailed Medium Article about this that I wrote for AngularInDepth.


Difference in Performance

I just did a Performance Audit on your Implementation as well as mine for the same set of steps performed on the App.

Here's the summary with your implementation:

enter image description here

And here's the summary with the refactored implementation:

enter image description here

SiddAjmera
  • 38,129
  • 5
  • 72
  • 110
  • thank you for your answer, could you expain whice one mean in your Preformance Audit ? – dam Feb 13 '19 at 06:48
  • 3
    I've already written a detailed Medium article about this. I'll share it soon enough and update my answer once done. Meanwhile, you can have a look at the Draft [here](https://blog.angularindepth.com/how-i-increased-the-performance-of-an-extremely-deeply-nested-angular-reactive-form-by-380-c1b18ea668db): – SiddAjmera Feb 13 '19 at 07:05
  • 1
    thank you , i just start follow you in Medium ,Plase tell me if you finish your blog i will share you blog , i think is useful to another – dam Feb 13 '19 at 07:16
  • i forget to tell something , i refactor my code is some
    to that help improve performance
    – dam Feb 13 '19 at 07:40
  • I'm working with huge forms either and can suggest you using classes for generating FormGroups. It's very powerful and convenient thing – Sergey Jun 12 '19 at 18:36
  • @SiddAjmera what could you suggest for those dealing with huge nested forms where the form reset is required? The question is about `FormArray`s. I see only two ways of dealing with them: create FormArray from scratch or using while make it empty and re-create it's child forms patching initial values to them. – Sergey Jun 12 '19 at 18:39
  • @Sergey, can you help me out with your scenario by creating a sample stackblitz to work with? – SiddAjmera Jun 13 '19 at 13:19
  • @SiddAjmera I did my best https://stackblitz.com/edit/angular-xzc7mc?file=src%2Fapp%2Fapp.component.ts Please note that this is an over simplified form example for the sake of saving time, because the real one can take up to day or two (I don't want to expose the real code so I would need to process it :) ) – Sergey Jun 13 '19 at 14:59
  • @Sergey can we move this into a new question? I'd also be interested in an answer to this. `this.formGroup.reset();` does not set FormArrays back to original and this suggests complexity https://stackoverflow.com/questions/41852183/angular-2-remove-all-items-from-a-formarray – Andrew Allen Jun 13 '19 at 15:59
  • 1
    What an answer! I'm impressed, @SiddAjmera. – giovannipds Feb 23 '21 at 20:36