📜 ⬆️ ⬇️

Angular Pitfalls and Time Saver

With Angular, you can do anything. Or almost everything. But sometimes this insidious "almost" leads to the fact that the developer is wasting time, creating workarounds, or trying to understand why something is happening, or why something does not work as expected.



The author of the article, the translation of which we are publishing today, says that he wants to share tips that will help Angular developers save some time. He is going to talk about the angular pitfalls with which he (and not only him) happened to meet.

â„–1. User 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 it:
')
<span awesomeTooltip="'Tooltip text'"> </span> 

You run the application ... And nothing happens. You, like any normal experienced programmer, look into the Chrome developer tools console. And you see nothing there. The directive does not work and Angular keeps silence.

Then some bright head from your team decides to put the directive in square brackets.

 <span [awesomeTooltip]="'Tooltip text'"> </span> 

After that, having lost a little time, we see the following in the console.


That's what's the matter: we just forgot to import the module with the directive

Now the cause of the problem is completely obvious: we simply forgot to import the directive module into the Angular application module.
From here we deduce an important rule: never use directives without square brackets.

Experiment with directives here .

â„–2. ViewChild returns undefined


Suppose you create a link to the text entry element described in the Angular template.

 <input type="text" name="fname" #inputTag> 

You are going, using the RxJS function fromEvent, to create a stream that will receive what is entered into the field. To do this, you will need a link to the input field, which can be obtained using the Angular ViewChild decorator:

 class SomeComponent implements AfterViewInit {    @ViewChild('inputTag') inputTag: ElementRef;    ngAfterViewInit(){        const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')    } ...   } 

Here we, using the RxJS function fromEvent, create a stream into which the data entered in the field will fall.
Test this code.


Mistake

What happened?

In fact, the following rule applies here: if ViewChild returns undefined , look in the *ngIf template.

 <div *ngIf="someCondition">    <input type="text" name="fname" #inputTag> </div> 

Here it is - the culprit of the problem.

In addition, check the template for the presence of other structural directives in it or the ng-template above the problem element.
Consider possible solutions to this problem.

â–Ť Option to solve problem â„–1


You can simply hide the template element in the event that 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 ngAfterViewInit hook.

 <div [hidden]="!someCondition">    <input type="text" name="fname" #inputTag> </div> 

â–Ť Option for solving problem # 2


Another way to solve this problem is to use setters.

 class SomeComponent {   @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 certain value to the inputTag property, we create a stream from the data entered in the input field.

Here are a couple of helpful resources related to this issue:


Number 3. Code execution when updating the list generated by * ngFor (after elements appear in the DOM)


Suppose you have some interesting user directive to organize scrollable lists. You are about to apply it to a list that is created using the Angular *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 scrollDirective.update to adjust the scrolling behavior taking into account the changes that occurred in the list.

It may seem that this can be done using the ngOnChanges hook:

 class SomeComponent implements OnChanges {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  ngOnChanges(changes) {    if (changes.itemsList) {      this.scroll.update();    }  } ... } 

True, here we meet the problem. The hook is invoked before the browser displays the updated list. As a result, recalculation of directive parameters for scrolling the list is performed incorrectly.

How to make a call immediately after *ngFor terminates?

You can do this by following these 3 simple steps:

â–ŤStep number 1


Put the links to the elements where *ngFor ( #listItems ) is #listItems .

 <div [customScroll]>    <p *ngFor="let item of items" #listItems>{{item}}</p> </div> 

â–ŤStep number 2


Get a list of these items using the Angular ViewChildren decorator. It returns an entity of type QueryList .

â–ŤStep number 3


The QueryList class has a read-only changes property that QueryList events each time the list changes.

 class SomeComponent implements AfterViewInit {  @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 solved. Here you can experiment with the appropriate example.

â„–4. Issues with ActivatedRoute.queryParam that occur when queries can be executed without parameters


The following code will help us to understand the essence of this problem.

 // app-routing.module.ts const routes: Routes = [    {path: '', redirectTo: '/home', pathMatch: 'full'},    {path: 'home', component: HomeComponent},  ];   @NgModule({    imports: [RouterModule.forRoot(routes)], //  #1    exports: [RouterModule]  })  export class AppRoutingModule { }    //app.module.ts  @NgModule({    ...    bootstrap: [AppComponent] //  #2  })  export class AppModule { }   // 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          });    }  } 

To some fragments of this code, comments of the form #x . Consider them:

  1. In the main application module, we defined routes and added a RouterModule there. The routes are configured so that if the route is not provided in the URL, we redirect the user to the /home page.
  2. As a component for download, we specify in the main module AppComponent .
  3. AppComponent uses <router-outlet> to output the corresponding route components.
  4. Now - the most important. We need to get the queryParams for the route from the URL

Suppose we got the following URL:

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

In this case, 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 received the parameters, everything works as expected ...

Take a closer look at the above screenshot of the browser screen and what is displayed in the console. Here you can see that the queryParams object is queryParams twice. The first object is empty, it is issued during the initialization process of the Angular router. Only after that we get an object containing the request 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 when executing the query without parameters is not issued

As a result, it turns out that if the code expects a second object from which it can get the request data, then it will not be launched if there were no request parameters in the URL.

How to solve this problem? This is where RxJs can help. We will create two observable objects based on ActivatedRoute.queryParams . As usual - consider a step by step solution to the problem.

â–ŤStep number 1


The first observable object, paramsInUrl$ , will output data if the queryParams value queryParams not empty:

 export class AppComponent implements OnInit {    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 output an empty value only if no request parameters were found in the URL:

 export class AppComponent implements OnInit {    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 let's combine the observed objects using the RxJS merge function:

 export class AppComponent implements OnInit {    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 param$ object param$ value only once - regardless of whether there is anything in the queryParams (an object with query parameters is issued) or not (an empty object is issued).

You can experiment with this code here .

â„–5. Slow work pages


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 by calling the formatItem method.
  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 problems. Therefore, run a performance test only in order to comply with all the formalities. However, during this test some strangeness appears.


Many formatItem calls and a rather heavy load on the processor

What's the matter? But the fact is that when Angular redraws the template, it also calls all the functions from the template (in our case, the formatItem function). As a result, if some heavy calculations are performed in the template functions, this will put a load on the processor and affect how users will perceive the corresponding page.

How to fix it? It is enough to perform the calculations performed in formatItem in advance and display the ready data 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 calls formatItem and low processor load

Now the application works much better. But the solution applied here has some features that are not always pleasant:


 * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; //      

If you are interested in improving the performance of Angular-applications - here , here , here and here - useful materials about it.

Results


Now, when you read this article, 5 new tools should appear in your arsenal of Angular-developer, with the help of which you will be able to solve some common problems. We hope the tips you found here will save you some time.

Dear readers! Do you know about something that, when developing Angular-based applications, helps save time?

Source: https://habr.com/ru/post/459304/


All Articles