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 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;
}
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 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) {
}
}
Step 4: Refactor your Template:
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_ri