Angular - Zoned Out !

suvi

Suvendu Karmakar

Posted on August 15, 2021

Angular - Zoned Out !

A short guide to understand ZoneJs and to solve a problem.


How about no upfront elucidation of the topic but a story. Okay here it goes - it was a regular morning at work. I as usual was sipping my coffee and trying to think of solution to a problem. The problem at hand was to implement a progress bar which tracks all the API calls before landing onto the actual page (loaded with the API data, Obviously!). As the application deals with myriads of data, this loader was to be designed to track lot of API calls. "How hard can it be?" , I thought. But the problem which seemed puny at the beginning, later turned out to be a nightmare.

Initially...


I was almost a novice in understanding how to use Angular's powerful tools to develop this. So,like any other problem I started looking out for possible ways to implement this in Angular. I read many blogs and came across several Stack Overflow posts. Everything I found was pretty much useless. None of them seemed efficient in the current context. There are no modules or libraries that accomplishes this. I started feeling worried. But I came up with a solution that made sense in my head and I was excited again.

Sometimes the simplest solution is the best, but sometimes it's not


The solution was straight forward. Create a progress-bar component and use a service to trigger the bar to move forward. Simple enough!

I started off by creating model class for the message :

export class Message {
    id: string;
    message: string;
    active: boolean;
}
Enter fullscreen mode Exit fullscreen mode

After the model is ready, next I created the progress loader component i.e. ProgressBarComponent :

// other imports 
import { Message } from '../../models/interfaces';

@Component({
  selector: 'progress-bar',
  templateUrl: './progress.bar.component.html',
  styleUrls: ['./progress.bar.component.scss']
})
export class ProgressBarComponent implements OnChanges {
  @Input() messages: Message[] = [];
  @Output() loadingEmitter = new EventEmitter<boolean>();

  constructor() { }

  public activeMessage: Message = { id: '', message: '', active: false };
  public progressCount = 0;

  ngOnChanges() {
   /* Code to check progress count and set the active message on the loader */ 

   /* Actual code removed for the sake of brevity. */
  }
}
Enter fullscreen mode Exit fullscreen mode

And the service to trigger the active message i.e. :

// other imports
import { Message } from '../../../../models/interfaces';

@Injectable({
  providedIn: 'root'
})
export class LoadTrackerService {

  constructor() {}

  public loaderMessages: Message[] = [
    { id : 'm_id_1', message: 'Load Started,API 1 called', active: true },
    { id : 'm_id_2', message: 'API 2 called', active: false },
    { id : 'm_id_3', message: 'API 3 called', active: false },
    { id : 'm_id_4', message: 'API 4 called', active: false }
    { id : 'm_id_5', message: 'API 5 called, Load Complete', active: false }
  ];

  public loadingPercent: number;
  public loading = true;
  public messageSubject = new BehaviorSubject<Message[]>(this.loaderMessages);

  setMessage(messageId: string) {
    if (this.activateMessage(messageId)) {
      this.messageSubject.next(this.loaderMessages);
    }
  }

  activateMessage(messageId: string): Boolean {
     /* Code to activate message on the loader and return boolean on 
        activation*/ 

     /* Actual code removed for the sake of brevity. */
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the messageSubject will be triggered by the APIService(service where all API calls are made) and is subscribed by the loader component to retrieve the active message and increment the loader. Everything made sense until I realized the real issue.

Off to a bad start


I soon realized that there was no way to track the API calls, all I could do is trigger the LoadTrackerService inside each method of APIService as below :


@Injectable({
  providedIn: 'root'
})
export class APIService {

  constructor(
   private loadTracker: LoadTrackerService) {}

  api_call_1() {
    /* Http call for API 1*/
    this.loadTracker.setMessage('m_id_1');
  }

   api_call_2() {
    /* Http call for API 2*/
    this.loadTracker.setMessage('m_id_2');
  }

  api_call_3() {
    /* Http call for API 3*/
    this.loadTracker.setMessage('m_id_3');
  }

  // and so on...

}
Enter fullscreen mode Exit fullscreen mode

Now this above solution can certainly be applied where there are not many API calls, but in an actual real world scenario with 100s of API calls, this approach would make the code dirty and repetitive. I needed something better and cleaner.

Get to the safe zone(js) ;)


So after a lot of researching and reading various in-depth articles on Angular, I came across this article. Turns out Angular handles and tracks all the API calls inside something called a Zone. Zones is the mechanism to handle logically connected async calls. Angular (or rather ZoneJS) conveniently calls them microtasks . It became very clear now, how to take advantage of this magic.

I started by creating a new Zone by forking the angular default zone and called it trackerZone. It is very important to place this Zone logic inside a resolver(Angular route resolver), so to trigger it and get it resolved before we land onto the actual page.


import { Injectable, NgZone } from '@angular/core';
// other imports...

@Injectable()
export class ProjectResolver implements Resolve<any> {
  constructor(
    private ngZone: NgZone,
    private loadTracker: LoadTrackerService,
  ) { }

  public trackerZone: NgZone;

  resolve() {
    return this.resolveInTrackerZone();
  }

  resolveInTrackerZone() {
    this.trackerZone = this.ngZone['_inner'].fork({
      properties: {
        countSchedule: 0,
        loaderRef: this.loadTracker
      },
      onScheduleTask(delegate, currentZone, targetZone, task) 
      {}   
    });

Enter fullscreen mode Exit fullscreen mode

Let me explain quickly what's happening here. For accessing the default Angular Zone, we can import it from 'angular/core'. So I have instantiated it into a private variable called ngZone, so that we use the zone reference for forking later. Next I have created my very own trackerZone .
Now we can fork the zone instance and assign it to our trackerZone.

Now we can pass properties / values / references to the trackerZone inside the properties object. Along with that we get a onScheduleTask callback method, which gets fired every time any task fires. The thing worth mentioning here is that, apart from microtasks there are different types of tasks, that we won't discuss here but are as also important. If you want to understand it better, I highly recommend this blog. The task is an object with various properties like type, data etc. (used below)

The next thing to do was to run all the API calls inside the tracker zone by using trackerZone.run() method. That's all you have to do, to get Angular fire the Zone magic and give us microtasks.



/
import { Injectable, NgZone } from '@angular/core';
// other imports...

@Injectable()
export class ProjectResolver implements Resolve<any> {
  constructor(
    private ngZone: NgZone,
    private loadTracker: LoadTrackerService,
  ) { }

  public trackerZone: NgZone;

  resolve() {
    return this.resolveInTrackerZone();
  }

  resolveInTrackerZone() {
    this.trackerZone = this.ngZone['_inner'].fork({
      properties: {
        countSchedule: 0,
        loaderRef: this.loadTracker
      },
      onScheduleTask(delegate, currentZone, targetZone, task) 
      {
        const result = delegate.scheduleTask(targetZone, 
        task);
        const url = task['data']['url'] || '';
        const tracker = this.properties.loaderRef;

        if (task.type === 'macroTask' && task._state !== 
        'unknown') {
           /* Triggering the message service based on URL */
        }
        return result;
      }
      }   
    });

    this.trackerZone.run(() => {
      /* return Observable / API call / Parallel Calls*/
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we scheduled the tasks manually inside our trackerZone using delegate.scheduleTask(targetZone, task) . Now we just have to map the URLs with the message ids and trigger the service.


if (task.type === 'macroTask' && task._state !== 'unknown') {
          this.properties.countSchedule += 1;
          if (url.indexOf('id_1') > -1) {
            tracker.setMessage('m_id_1');
          } else if (url.indexOf('id_2') > -1) {
            tracker.setMessage('m_id_2');
          } else if (url.indexOf('id_3') > -1) {
            tracker.setMessage('id_3');
          }

          // and so on...
        }

Enter fullscreen mode Exit fullscreen mode

That's all there is ! I really loved how easy and convenient ZoneJS makes this whole process. Just to cover all the bases, another way of doing this could be using HTTP interceptors, but I feel ZoneJS is much more elegant and intuitive. Again it's just my opinion.


Lastly, this is my first blog post ever. Don't hate me for being a noob at writing, I will get better. Please drop some kind words in the comments below, if you like it.

Peace 🖖


💖 💪 🙅 🚩
suvi
Suvendu Karmakar

Posted on August 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Angular - Zoned Out !
angular Angular - Zoned Out !

August 15, 2021