Reputation: 431
I have an Angular form in a Material Dialog component. There is data being two-way binded and when tabbing through inputs or typing in inputs results in the screen locking up for a few seconds between keydown's . All data is passing properly but is painfully slow while trying to use the form.
I've tried refactoring the form to use for the inputs to use a "Material form" but still has the same slowdown performance.
Here's a screenshot of performance tracker in chrome:
Is there something wrong with my configuration? Or is this a possible regression in latest Angular 8 animation / CDK packages? Here are my Angular package dependencies:
dependencies": {
"@angular/animations": "^8.2.13",
"@angular/cdk": "^8.2.3",
"@angular/common": "~8.2.13",
"@angular/compiler": "~8.2.13",
"@angular/core": "~8.2.13",
"@angular/forms": "~8.2.13",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "~8.2.13",
"@angular/platform-browser-dynamic": "~8.2.13",
"@angular/router": "~8.2.13",
}
Here is the component method that calls the dialog:
public editRow(tablerow: IRule): void {
const dialogRef = this.dialog.open(EditDialogComponent, {
width: '100%',
height: '85%',
data: tablerow
});
this.subscriptions.push(
dialogRef.afterClosed().subscribe(updatedRule => {
if (updatedRule !== undefined) {
this.rules = this.rules.map(rule => rule.Id === updatedRule.Id ? updatedRule : rule);
this.subscriptions.push(this.dataService.updateRule(updatedRule).subscribe(
response => {
this.snackBar.openFromComponent(SuccessComponent, {
duration: 3000,
data: `Rule added`
});
}, error => {
this.snackBar.openFromComponent(ErrorComponent, {
duration: 10000,
data: 'Internal Server Error'
});
}
));
}
})
);
}
The mat dialog template containing the form:
<mat-dialog-content>
<i id="close-icon" class="material-icons md-24" aria-label="close"
[mat-dialog-close]>close</i>
<div class="brand-panel-container">
<div class="brand-panel">
<div class="brand-panel-header">
<div class="brand-title">
<h4 mat-dialog-title>Rule: {{ data.Id }}</h4>
</div>
</div>
<form #ruleForm="ngForm">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Shop Type:<span class="asterisk">*</span></label>
<select
[(ngModel)]="data.Type.Text"
value="{{ data.Type.Text }}"
name="type"
type="text"
class="form-control"
id="type"
required>
<option *ngFor="let opt of shopTypeOpts; trackBy: indentify" value="{{opt.Text}}">{{opt.Text}}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Origin:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.Origin"
value="{{ data.Origin }}"
name="origin"
type="text"
class="form-control"
id="origin"
required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Destination:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.Destination"
value="{{ data.Destination }}"
name="destination"
type="text"
class="form-control"
id="destination"
required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Fare:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.Fare.Text"
value="{{ data.Fare.Text }}"
name="fare"
type="text"
class="form-control"
id="fare"
required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Government:<span class="asterisk">*</span></label>
<select
[(ngModel)]="data.Government.Text"
value="{{ data.Government.Text }}"
name="government"
type="text"
class="form-control"
id="government"
required>
<option *ngFor="let opt of governmentTypeOpts; trackBy: indentify"
value="{{opt.Text}}">{{opt.Text}}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Special Pricing:<span class="asterisk">*</span></label>
<select
[(ngModel)]="data.SpecialPricing.Text"
value="{{ data.SpecialPricing.Text }}"
name="specialPricing"
type="text"
class="form-control"
id="specialPricing"
required>
<option *ngFor="let opt of specialPricingTypeOpts; trackBy: indentify"
value="{{opt.Text}}">{{opt.Text}}</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Upgrade:<span class="asterisk">*</span></label>
<select
[(ngModel)]="data.Upgrade.Text"
value="{{ data.Upgrade.Text }}"
name="upgrade"
type="text"
class="form-control"
id="upgrade"
required>
<option *ngFor="let opt of upgradeTypeOpts; trackBy: indentify"
value="{{opt.Text}}">{{opt.Text}}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Cabin Count:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.CabinCount"
value="{{ data.CabinCount }}"
name="cabinCount"
type="text"
class="form-control"
id="cabinCount"
required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Columns Count:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.ColumnsCount"
value="{{ data.ColumnsCount }}"
name="columnsCount"
type="text"
class="form-control"
id="columnsCount"
required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Lang Code:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.LangCode"
value="{{ data.LangCode }}"
name="langCode"
type="text"
class="form-control"
id="langCode"
required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">Fare Wheel Search?:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.IsFareWheelSearch"
value="{{ data.IsFareWheelSearch }}"
name="isFareWheelSearch"
type="text"
class="form-control"
id="isFareWheelSearch"
required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Markets:<span class="asterisk">*</span></label>
<select
[(ngModel)]="data.Markets.Text"
value="{{ data.Markets.Text }}"
name="markets"
type="text"
class="form-control"
id="markets"
required>
<option *ngFor="let opt of marketTypeOpts; trackBy: indentify" value="{{opt.Text}}">{{opt.Text}}</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name">POS:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.POS"
value="{{ data.POS }}"
name="pos"
type="text"
class="form-control"
id="pos"
required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="name">Columns:<span class="asterisk">*</span></label>
<input
[(ngModel)]="data.Columns"
value="{{ data.Columns }}"
name="columns"
type="text"
class="form-control"
id="columns"
required>
</div>
</div>
</div>
<div mat-dialog-actions>
<span *ngIf="!ruleForm.valid" class="invalid-msg"><span
class="asterisk">*</span>All fields must be filled in to save
changes.</span>
<button mat-button class="brand-default-button"
[mat-dialog-close]>Cancel</button>
<button mat-button class="brand-confirm-button" type="submit"
[disabled]="!ruleForm.valid" [mat-dialog-close]="data.Id"
(click)="onSaveData(ruleForm.value)">Save Changes</button>
</div>
</form>
</div>
The dialog component file:
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { IRule } from '../../../models/rule.interface';
import { OptionsService } from 'src/app/shared/services/options.service';
import { IDropdownOption } from 'src/models/dropdown-option.interface';
@Component({
selector: 'app-edit-dialog',
templateUrl: './edit-dialog.component.html',
styleUrls: ['./edit-dialog.component.scss']
})
export class EditDialogComponent {
public shopTypeOpts: IDropdownOption[] = [];
public governmentTypeOpts: IDropdownOption[] = [];
public specialPricingTypeOpts: IDropdownOption[] = [];
public fareTypeOpts: IDropdownOption[] = [];
public upgradeTypeOpts: IDropdownOption[] = [];
public marketTypeOpts: IDropdownOption[] = [];
constructor(
public dialogRef: MatDialogRef<EditDialogComponent>,
public optionsService: OptionsService,
@Inject(MAT_DIALOG_DATA) public data: IRule) {
this.shopTypeOpts = this.optionsService.shopTypeOptions;
this.governmentTypeOpts = this.optionsService.governmentTypeOptions;
this.specialPricingTypeOpts = this.optionsService.specialPricingTypeOptions;
this.fareTypeOpts = this.optionsService.fareTypeOptions;
this.upgradeTypeOpts = this.optionsService.upgradeTypeOptions;
this.marketTypeOpts = this.optionsService.marketTypeOptions;
}
public onSaveData(updatedRule: IRule): void {
this.dialogRef.close(updatedRule);
}
public indentify(index, item) {
return item.Text;
}
}
IDropdownOption interface:
export interface IDropdownOption {
Text: string;
Value: number;
}
*EDITED to include trackBy function & IDropdownOption interface to see unique identifier. *
The slow down seems to be because of the dropdown options being looped over repeatedly... Maybe changeDetection strategy needs to be changed?
Upvotes: 4
Views: 8890
Reputation: 3166
This answer is use case dependent. Might sound weird!
Background: I have implemented OnPush Strategy as well detaching reattaching ChangeDetectorRef in my custom grid/table component where few elements have glass overlay added using directive with help of renderer2
I had added blur background as 'backdropClass' property while opening dialog.
It caused a lag when the screen size is more, If I reduce screen size it's performing well. If I delete few elements from the Dom tree it performs well.
So I found it was backdrop class that was slowly applied, resulting in an illusion of lag on the overall dialog.
Just remove it.
Hope this helps someone who is scratching head over all solutions already implemented :)
this.dialog
.open(CreateKeyComponent, {
disableClose: true,
// backdropClass: 'blur', -----> root cause
position: { top: '15vh' },
data: {
data: this.tableDataShowFlag ? '1' : '0',
},
})
Upvotes: 1
Reputation: 431
Thanks so much for everyone's help. I solved my issue by using ChangeDetectorRef in the parent component to detach once the dialog is opened, and reattached once the dialog is closed. This prevents any re-render / re-drawing of the EditDialogComponent and fixes the performance issue.
public editRow(tablerow: IRule): void {
this.changeDetectorRef.detach(); // Detach change detection before the dialog opens.
const dialogRef = this.dialog.open(EditDialogComponent, {
width: '100%',
height: '85%',
data: tablerow
});
this.subscriptions.push(
dialogRef.afterClosed().subscribe(updatedRule => {
this.changeDetectorRef.reattach(); // Reattach change detection after the dialog closes.
if (updatedRule !== undefined) {
this.rules = this.rules.map(rule => rule.Id === updatedRule.Id ? updatedRule : rule);
this.subscriptions.push(this.dataService.updateRule(updatedRule).subscribe(
response => {
this.snackBar.openFromComponent(SuccessComponent, {
duration: 3000,
data: `Rule added`
});
}, error => {
this.snackBar.openFromComponent(ErrorComponent, {
duration: 10000,
data: 'Internal Server Error'
});
}
));
}
})
);
}
Upvotes: 7
Reputation: 71911
I'll make it an answer, because I think I know what's causing your slowdown. Besides the lack of trackBy
method for your *ngFor
, it also has to check the entire template with every input you do. It's advised to use the OnPush
change detection strategy. This might make some things not work the way you expect them to, but it's a very good way to keep your components quick, because changes will only be checked once an Input is changed (and other things.
This is still a good article on this subject.
You should change your dialog component decorator to include OnPush
:
@Component({
selector: 'app-edit-dialog',
templateUrl: './edit-dialog.component.html',
styleUrls: ['./edit-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
Upvotes: 3