Angular Pitfall Bypass and Time Saving
- Transfer
With Angular, you can do anything. Or almost everything. But sometimes this insidious “almost” leads to the fact that the developer is wasting time by creating workarounds, or trying to understand why something is happening, or why something is not working as expected.
The author of the article we are translating today says he wants to share tips that will help Angular developers save some time. He is going to talk about the pitfalls of Angular, which he (and not only him) had a chance to meet.
So, you found a nice third-party Angular directive and decided to use it with standard elements in the Angular template. Wonderful! Let's try to do this:
You start the application ... And nothing happens. You, like any normal, experienced programmer, look into the Chrome Developer Tools console. And you don’t see anything there. The directive does not work and Angular is silent.
Then some bright head from your team decides to place the directive in square brackets.
After that, having lost a little time, we see the following in the console.
Here's the thing: we just forgot to import the module with the directive
Now the cause of the problem is completely obvious: we just forgot to import the directive module into the Angular application module.
This leads to an important rule: never use directives without square brackets.
You can experiment with directives here .
Suppose you create a link to a text entry element described in an Angular template.
You are going, using the RxJS fromEvent function , to create a stream into which what is entered into the field will fall. To do this, you will need a link to the input field, which can be obtained using the Angular decorator
Here, using the RxJS function
Test this code.
Error
What happened?
As a matter of fact, the following rule applies here: if it
Here he is the culprit of the problem.
In addition, check the template for the presence of other structural directives in it or
Consider possible solutions to this problem.
You can simply hide the template element if you do not need it. In this case, the element will always continue to exist and
Another way to solve this problem is to use setters.
Here, as soon as Angular assigns a
Here are a couple of useful resources related to this issue:
Suppose you have some interesting custom directive for organizing scrollable lists. You are going to apply it to a list that is created using the Angular directive
Usually, in such cases, when updating the list, you need to call something like that
It might seem that this can be done using a hook
True, here we are faced with a problem. The hook is called before the browser displays the updated list. As a result, recalculation of the directive's parameters for scrolling the list is performed incorrectly.
How to make a call right after it
You can do this by following these 3 simple steps:
Place links to elements where
Get a list of these elements using the Angular decorator
The class
Now the problem is resolved. Here you can experiment with the corresponding example.
The following code will help us understand the essence of this problem.
Some fragments of this code have comments of the form
Suppose we got this URL:
In this case, it
Let's look at the work of all this in the browser.
Testing an application that implements a routing system
Here you may have a question about the essence of the problem. We got the parameters, everything works as expected ...
Take a closer look at the above screenshot of the browser, and what is displayed in the console. Here you can see that the object
The problem is that if there are no request parameters in the URL, the router will not return anything. That is, after issuing the first empty object, the second object, also empty, which could indicate the absence of parameters, will not be issued.
The second object is not issued when the request is executed without parameters.
As a result, it turns out that if the code expects a second object from which it can receive the request data, it will not be launched if the URL did not have request parameters.
How to solve this problem? Here RxJs can help us. We will create
The first observed object
The second observable object
Now combine the observed objects using the RxJS merge function :
Now the observed object
You can experiment with this code here .
Suppose you have a component that displays some formatted data:
This component solves two problems:
You do not expect this component to have any performance issues. Therefore, run a performance test only to comply with all the formalities. However, some oddities appear during this test.
A lot of formatItem calls and a rather big processor load.
What's the matter? But the fact is that when Angular redraws the template, it calls all the functions from the template (in our case, the function
How to fix it? It is enough to perform the calculations performed in
Now the performance test looks much more decent.
Only 6 formatItem calls and low processor load.
Now the application works much better. But the solution used here has some features that are not always pleasant:
If you are interested in the issues of improving the performance of Angular-applications - here , here , here and here - useful materials about this.
Now that you have read this article, 5 new tools should appear in your Angular developer arsenal with which you can solve some common problems. We hope the tips you found here help you save some time.
Dear readers! Do you know anything that helps you save time when developing Angular applications?
No. 1. The custom directive you applied does not work
So, you found a nice third-party Angular directive and decided to use it with standard elements in the Angular template. Wonderful! Let's try to do this:
<span awesomeTooltip="'Tooltip text'"> </span>
You start the application ... And nothing happens. You, like any normal, experienced programmer, look into the Chrome Developer Tools console. And you don’t see anything there. The directive does not work and Angular is silent.
Then some bright head from your team decides to place the directive in square brackets.
<span [awesomeTooltip]="'Tooltip text'"> </span>
After that, having lost a little time, we see the following in the console.
Here's the thing: we just forgot to import the module with the directive
Now the cause of the problem is completely obvious: we just forgot to import the directive module into the Angular application module.
This leads to an important rule: never use directives without square brackets.
You can experiment with directives here .
No. 2. ViewChild returns undefined
Suppose you create a link to a text entry element described in an Angular template.
<input type="text" name="fname" #inputTag>
You are going, using the RxJS fromEvent function , to create a stream into which what is entered into the field will fall. To do this, you will need a link to the input field, which can be obtained using the Angular decorator
ViewChild
:classSomeComponentimplementsAfterViewInit{
@ViewChild('inputTag') inputTag: ElementRef;
ngAfterViewInit(){
const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')
}
...
}
Here, using the RxJS function
fromEvent
, we create a stream into which the data entered into the field will fall. Test this code.
Error
What happened?
As a matter of fact, the following rule applies here: if it
ViewChild
returnsundefined
, look in the template*ngIf
.<div *ngIf="someCondition">
<inputtype="text"name="fname" #inputTag></div>
Here he is the culprit of the problem.
In addition, check the template for the presence of other structural directives in it or
ng-template
higher than the problem element. Consider possible solutions to this problem.
▍Variant of solving the problem №1
You can simply hide the template element if you do not need it. In this case, the element will always continue to exist and
ViewChild
will be able to return a link to it in the hook ngAfterViewInit
.<div [hidden]="!someCondition">
<inputtype="text"name="fname" #inputTag></div>
▍Variant of solution to problem No. 2
Another way to solve this problem is to use setters.
classSomeComponent{
@ViewChild('inputTag') set inputTag(input: ElementRef|null) {
if(!input) return;
this.doSomething(input);
}
doSomething(input) {
const input$ = keysfromEvent(input.nativeElement, 'keyup');
...
}
}
Here, as soon as Angular assigns a
inputTag
specific value to the property , we create a stream from the data entered in the input field. Here are a couple of useful resources related to this issue:
- Here you can read that the results
ViewChild
in Angular 8 can be static and dynamic. - If you are having difficulty working with RxJs - take a look at this video course.
Number 3. Code execution when updating the list generated with * ngFor (after the elements appeared in the DOM)
Suppose you have some interesting custom directive for organizing scrollable lists. You are going to apply it to a list that is created using the Angular directive
*ngFor
.<div *ngFor="let item of itemsList; let i = index;"
[customScroll]
>
<p *ngFor="let item of items"class="list-item">{{item}}</p>
</div>
Usually, in such cases, when updating the list, you need to call something like that
scrollDirective.update
to configure the scrolling behavior, taking into account the changes that have occurred in the list. It might seem that this can be done using a hook
ngOnChanges
:classSomeComponentimplementsOnChanges{
@Input() itemsList = [];
@ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;
ngOnChanges(changes) {
if (changes.itemsList) {
this.scroll.update();
}
}
...
}
True, here we are faced with a problem. The hook is called before the browser displays the updated list. As a result, recalculation of the directive's parameters for scrolling the list is performed incorrectly.
How to make a call right after it
*ngFor
finishes work? You can do this by following these 3 simple steps:
▍Step number 1
Place links to elements where
*ngFor
( #listItems
) is applied .<div [customScroll]>
<p *ngFor="let item of items" #listItems>{{item}}</p>
</div>
▍Step number 2
Get a list of these elements using the Angular decorator
ViewChildren
. It returns the type entity QueryList
.▍Step number 3
The class
QueryList
has a read-only changes property that throws events every time the list changes.classSomeComponentimplementsAfterViewInit{
@Input() itemsList = [];
@ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;
@ViewChildren('listItems') listItems: QueryList<any>;
private sub: Subscription;
ngAfterViewInit() {
this.sub = this.listItems.changes.subscribe(() =>this.scroll.update())
}
...
}
Now the problem is resolved. Here you can experiment with the corresponding example.
Number 4. Issues with ActivatedRoute.queryParam occurring when queries can be run without parameters
The following code will help us understand the essence of this problem.
// app-routing.module.tsconst routes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
];
@NgModule({
imports: [RouterModule.forRoot(routes)], // Фрагмент #1
exports: [RouterModule]
})
exportclassAppRoutingModule{ }
//app.module.ts
@NgModule({
...
bootstrap: [AppComponent] // Фрагмент #2
})
exportclassAppModule{ }
// app.component.html
<router-outlet></router-outlet> // Фрагмент #3
// app.component.ts
export class AppComponent implements OnInit {
title = 'QueryTest';
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.queryParams
.subscribe(params => {
console.log('saveToken', params); // Фрагмент #4
});
}
}
Some fragments of this code have comments of the form
Фрагмент #x
. Consider them:- In the main module of the application, we defined the routes and added them there
RouterModule
. Routes are configured so that if a route is not provided in the URL, we redirect the user to the page/home
. - As a component for downloading, we specify in the main module
AppComponent
. AppComponent
uses<router-outlet>
to output the corresponding components of the route.- Now the most important thing. We need to get
queryParams
for the route from the URL
Suppose we got this URL:
https://localhost:4400/home?accessToken=someTokenSequence
In this case, it
queryParams
will look like this:{accessToken: ‘someTokenSequence’}
Let's look at the work of all this in the browser.
Testing an application that implements a routing system
Here you may have a question about the essence of the problem. We got the parameters, everything works as expected ...
Take a closer look at the above screenshot of the browser, and what is displayed in the console. Here you can see that the object
queryParams
is issued twice. The first object is empty; it is issued during the initialization process of the Angular router. Only after that we get an object that contains the query parameters (in our case -{accessToken: ‘someTokenSequence’}
). The problem is that if there are no request parameters in the URL, the router will not return anything. That is, after issuing the first empty object, the second object, also empty, which could indicate the absence of parameters, will not be issued.
The second object is not issued when the request is executed without parameters.
As a result, it turns out that if the code expects a second object from which it can receive the request data, it will not be launched if the URL did not have request parameters.
How to solve this problem? Here RxJs can help us. We will create
ActivatedRoute.queryParams
two observable objectsbased on. As usual - consider a step-by-step solution to the problem.▍Step number 1
The first observed object
paramsInUrl$
,, will return data if the value is queryParams
not empty:exportclassAppComponentimplementsOnInit{
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
// Отфильтровываем первое пустое значение
// И повторно выдаём значение с параметрами
const paramsInUrl$ = this.route.queryParams.pipe(
filter(params =>Object.keys(params).length > 0)
);
...
}
}
▍Step number 2
The second observable object
noParamsInUrl$
,, will return an empty value only if no request parameters were found in the URL:exportclassAppComponentimplementsOnInit{
title = 'QueryTest';
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
...
// Выдаёт пустой объект, но только в том случае, если в URL
// не было обнаружено параметров запроса
const noParamsInUrl$ = this.route.queryParams.pipe(
filter(() => !this.locationService.path().includes('?')),
map(() => ({}))
);
...
}
}
▍Step number 3
Now combine the observed objects using the RxJS merge function :
exportclassAppComponentimplementsOnInit{
title = 'QueryTest';
constructor(private route: ActivatedRoute,
private locationService: Location) {
}
ngOnInit() {
// Отфильтровываем первое пустое значение
// И повторно выдаём значение с параметрами
const paramsInUrl$ = this.route.queryParams.pipe(
filter(params =>Object.keys(params).length > 0)
);
// Выдаёт пустой объект, но только в том случае, если в URL
// не было обнаружено параметров запроса
const noParamsInUrl$ = this.route.queryParams.pipe(
filter(() => !this.locationService.path().includes('?')),
map(() => ({}))
);
const params$ = merge(paramsInUrl$, noParamsInUrl$);
params$.subscribe(params => {
console.log('saveToken', params);
});
}
}
Now the observed object
param$
returns a value only once - regardless of whether something is contained in queryParams
(an object with query parameters is returned) or not (an empty object is returned). You can experiment with this code here .
No. 5. Page Slow
Suppose you have a component that displays some formatted data:
// home.component.html
<div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">
<div *ngFor="let item of items">
<span>{{formatItem(item)}}</span>
</div>
</div>
{{mouseCoordinates | json}}
// home.component.ts
export class HomeComponent {
items = [1, 2, 3, 4, 5, 6];
mouseCoordinates = {};
formatItem(item) {
// Имитация тяжёлых вычислений
const t = Array.apply(null, Array(5)).map(() => 1);
console.log('formatItem');
return item + '%';
}
}
This component solves two problems:
- It displays an array of elements (it is assumed that this operation is performed once). In addition, it formats what is displayed on the screen by calling a method
formatItem
. - It displays the coordinates of the mouse (this value will obviously be updated very often).
You do not expect this component to have any performance issues. Therefore, run a performance test only to comply with all the formalities. However, some oddities appear during this test.
A lot of formatItem calls and a rather big processor load.
What's the matter? But the fact is that when Angular redraws the template, it calls all the functions from the template (in our case, the function
formatItem
). As a result, if any heavy calculations are performed in the template functions, this puts a strain on the processor and affects how users perceive the corresponding page. How to fix it? It is enough to perform the calculations performed in
formatItem
advance, and display the data already ready on the page.// home.component.html
<div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">
<div *ngFor="let item of displayedItems">
<span>{{item}}</span>
</div>
</div>
{{mouseCoordinates | json}}
// home.component.ts
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.sass']
})
export class HomeComponent implements OnInit {
items = [1, 2, 3, 4, 5, 6];
displayedItems = [];
mouseCoordinates = {};
ngOnInit() {
this.displayedItems = this.items.map((item) => this.formatItem(item));
}
formatItem(item) {
console.log('formatItem');
const t = Array.apply(null, Array(5)).map(() => 1);
return item + '%';
}
}
Now the performance test looks much more decent.
Only 6 formatItem calls and low processor load.
Now the application works much better. But the solution used here has some features that are not always pleasant:
- Since we display the coordinates of the mouse in the template - the occurrence of an event
mousemove
still leads to the start of a change check. But, since we need the coordinates of the mouse, we cannot get rid of this. - If, in the event handler
mousemove
, only certain calculations should be performed (which do not affect what is displayed on the page), then, to speed up the application, you can do the following:- You can use it inside the event handler function
NgZone.runOutsideOfAngular
. This prevents the change check from starting when an event occursmousemove
(this will only affect this handler). - You can prevent the zone.js patch for some events by using the following line of code in polyfills.ts . This will affect the entire Angular application.
- You can use it inside the event handler function
* (windowas any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // отключить патч для указанных событий
If you are interested in the issues of improving the performance of Angular-applications - here , here , here and here - useful materials about this.
Summary
Now that you have read this article, 5 new tools should appear in your Angular developer arsenal with which you can solve some common problems. We hope the tips you found here help you save some time.
Dear readers! Do you know anything that helps you save time when developing Angular applications?