Angular Pitfall Bypass and Time Saving

Original author: Alexander Poshtaruk
  • 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.





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 itViewChildreturnsundefined, 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-templatehigher 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 ViewChildwill 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 inputTagspecific 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 ViewChildin 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.updateto 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 *ngForfinishes 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 QueryListhas 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:

  1. 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.
  2. As a component for downloading, we specify in the main module AppComponent.
  3. AppComponentuses <router-outlet>to output the corresponding components of the route.
  4. Now the most important thing. We need to get queryParamsfor the route from the URL

Suppose we got this URL:

https://localhost:4400/home?accessToken=someTokenSequence

In this case, it queryParamswill 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 objectqueryParamsis 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 createActivatedRoute.queryParamstwo 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 queryParamsnot 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:

  1. 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.
  2. 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 functionformatItem). 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 informatItemadvance, 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 mousemovestill 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:

    1. You can use it inside the event handler function NgZone.runOutsideOfAngular. This prevents the change check from starting when an event occurs mousemove(this will only affect this handler).
    2. 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.

* (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?


Also popular now: