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.
Reactive forms are more robust: they're more scalable, reusable, and testable. If forms are a key part of your application, or you're already using reactive patterns for building your application, use reactive forms. Template-driven forms are useful for adding a simple form to an app, such as an email list signup form.
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.
Reactive forms provide a model-driven approach to handling form inputs whose values change over time. This guide shows you how to create and update a basic form control, progress to using multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at run time.
A control is pristine if the user has not yet changed the value in the UI. A control is dirty if the user has changed the value in the UI. True if the control is marked as touched .
Reactive form programming in Angular 7. In Angular, there are two ways to work with web forms: template driven forms and reactive forms. In template driven form, we use angular directives to build the internal representation of form in the html template file.
In Angular, there are two ways to work with web forms: template driven forms and reactive forms. In template driven form, we use angular directives to build the internal representation of form in the html template file.
To use the reactive form controls, import ReactiveFormsModule from the @angular/forms package and add it to your NgModule’s imports array. // app.module.ts import { ReactiveFormsModule } from '@angular/forms' ; imports: [ ...
Angular is a platform for building mobile and desktop web applications. ... This guide shows you how to create and update a basic form control, progress to using multiple controls in a group, validate form values, and create dynamic forms where you can add or remove controls at run time.
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.
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 interface
s 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;
}
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]
});
}
}
Do it to get the Form and get rid of the methods
that would return to you the FormArray
s 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) {
}
}
And this one is THE MOST IMPORTANT. NEVER call getters or methods in deeply nested forms to get the FormArray
s. 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>
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.
I just did a Performance Audit on your Implementation as well as mine for the same set of steps performed on the App.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With