Thursday 2 June 2022

Angular lazy loading directive with IntersectionObserver

 


@angular/cli: 13.3.0
Source: GitHub

So let's dig into lazy loading once again! Previously, I had written an article Angular infinite scrolling using RxJs and NgRx, which explained the concept of loading more data on reaching the end-of page. But this time, the concept is wider, or more generic.

This time, I would like to reach a certain part of the viewport, and then load some data specific to that part. So I would observe a specific HTML selector and then do something when that selector comes into view. And that selector could be anywhere in the page, not just the end, something like this:

Preview


So of course, we go for the IntersectionObserver API. Almost all modern browsers now support it, so we should ideally have no problem for a modern website.

Let us once again break down the requirements, one by one.
1) We need a custom directive, which we can place on any HTML element.
2) This directive will accept a callback method as a parameter, so that the parent component consuming the directive can decide what to do once the child element comes into view.
3) Tracking should happen only once, so that every time the element comes into view (due to user scrolling up and down), the data should not be re-fetched. But this is my specific use-case, you may choose to do differently.
4) The observer should be disconnected when the element is destroyed.

The directive:

Whenever we place a directive on an element, we get a reference to the consuming element. And to be honest, this is all we need.
Almost the entire logic happens in the constructor, since each directive placed on each element creates a new instance of the class.

constructor(
  @Inject(DOCUMENT) private readonly document: Document,
  // eslint-disable-next-line @typescript-eslint/ban-types
  @Inject(PLATFORM_ID) private readonly platformId: Object,
  private readonly elementRef: ElementRef
) {
  if (isPlatformBrowser(this.platformId)) {
   const options = {
     root: this.document,
     rootMargin: '0px',
     threshold: 1.0,
   };

   const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
     entries.forEach((entry) => {
       if (entry.isIntersecting) {
         if (entry.intersectionRatio === 1 && !this.tracked) {
           this.appTrackable?.();
           this.tracked = true;
         }
       }
     });
   };

   this.observer = new IntersectionObserver(intersectionCallback, options);

   const target = this.elementRef?.nativeElement;
   target && this.observer?.observe(target);
 }
}

Let's understand what is being done:

The Options:
root - this essentially means the relative component against which your component - the one you would like to observe - should intersect. For our use case, this is the entire document.
rootMargin - this would consider if the document has some margin, so that the intersection logic would consider that during its calculation.
threshold - this is a value between 0 and 1 (or consider 0 and 100%). A value of 1 means, the API would consider my component to be intersecting with the document only when 100% of the element is in view. You can change this as per your need.

So what happens next?
Depending on the threshold value, the entry.isIntersecting evaluates to true or false, and it is then that we need to do something.

And what do we do then?
Our directive takes in a callback as an input, and we fire this callback - this.appTrackable?.();

Also, I mentioned before that each directive on each element is a new instance. So we maintain a class level variable - tracked. And once the intersection logic is satisfied and the callback is fired, we do not need to fire the logic again and again. You could go one step ahead and disconnect the observer itself.

When the component gets destroyed, so does the directive, and you can tap onto the ngOnDestroy method, to disconnect the observer.

ngOnDestroy(): void {
 this.observer?.disconnect();
}


The implementation:

The implementation means placing the directive on an element and then passing a callback reference to it.

home.component.html
<app-people [appTrackable]="callback1"></app-people>
<app-place [appTrackable]="callback2"></app-place>

The callback methods include some logic to fire HTTP calls to fetch data for that element, but that is not what this article is about and so I will not go into its details. But you can always check it out from the source code. Just remember to bind your callback to the service or class instance, as applicable.

home.component.ts
export class HomeComponent {
 constructor(private readonly data: DataService) {}

 readonly callback1 = this.data.callback.bind(this.data, 'people');
 readonly callback2 = this.data.callback.bind(this.data, 'places');
}


Cheers! :-)

Friday 13 May 2022

Handle CAPS-LOCK on/off event in Angular

Highlight

@angular/cli: 13.3.0
Source: GitHub

This is an interesting, yet quite commonly expected, use-case - warn the user that the CAPS-LOCK is ON, specially while entering a password, so that the user is aware of it and can change it, if needed.

Preview


There are a few scenarios handled in this demonstration:

1. Change the message when the user toggles the caps-lock while on the password field.
2. Also change the message when the user changes the caps-lock state elsewhere (maybe in another application or browser tab, etc) and then comes back to this application.
3. Remove the events when component is destroyed, to prevent memory leaks.
4. Run change-detection when ChangeDetectionStrategy is OnPush.

Let's see about 1 & 2.

By the way, I am on @angular/cli 13.3.0 and @angular/material 13.3.6. For the message, I have used mat-hint on mat-form-field.

To achieve this, we grab the reference of the password field (using @ViewChild) and then add a couple of event handlers to the underlying HTMLInputElement - keyup and mousedown.

@ViewChild('passwordField') passwordField?: ElementRef<HTMLInputElement>;

The keyup event handler will toggle the message when the user toggles the CAPS-LOCK button while being on the password field. This generates a KeyboardEvent.

The mousedown event handler will toggle the message when the user changes the state elsewhere and the navigates back to the application and clicks on the password field. This will be beneficial for the user since the information is now provided even before something is typed. This is done through a MouseEvent.

ngAfterViewInit(): void {
const eventHandler = (event: KeyboardEvent | MouseEvent) => {
    // if the password field is auto-filled by a password manager,
    // this will also fire a 'keyup' event, but this event will not
    // have the getModifierState method. So we will ignore this!
    if (event?.type === 'keyup' && !(event instanceof KeyboardEvent)) {
      return;
    }
    if (event.getModifierState('CapsLock')) {
      this.capsLockMessage = 'Caps Lock is ON';
    } else {
      this.capsLockMessage = undefined;
    }
    this.cdRef.markForCheck();
  };

  this.passwordField?.nativeElement?.addEventListener('keyup', eventHandler);
  this.passwordField?.nativeElement?.addEventListener(
    'mousedown',
    eventHandler
  );
}

Some improvements:

To prevent memory leaks, we need to remove these event handlers once the component is destroyed.

ngOnDestroy(): void {
  this.passwordField?.nativeElement?.removeAllListeners?.('keyup');
  this.passwordField?.nativeElement?.removeAllListeners?.('mousedown');
}

If the ChangeDetectionStrategy is set to OnPush, we need to mark the component for a change detection whenever either of the above events are fired. So, at the end of the event handler, use:

this.cdRef.markForCheck();

The browser's password manager may auto-fill the password data for you. This will also fire a keyup event, but this will not be an instance of KeyboardEvent and will thus not have the getModifierState method. So we will ignore this!

if (event?.type === 'keyup' && !(event instanceof KeyboardEvent)) {
return;
}

Cheers!

Tuesday 3 August 2021

Angular dynamic components with code-splitting

@angular/cli: 12.1.3
Source: GitHub

A lot of times, angular components need to be determined and displayed on the fly. A very common use-case is when the components are determined from a content-management-system (CMS). This is particularly useful when we allow the customer to control the content and layout of the page.


Well we do have *ngIf for that, don't we? Depending on some CMS field value, we can use *ngIf to determine which component needs to be displayed. This is cool, but there is a major downside to this approach. The problem is, we usually have a large component library, which need to be loaded eagerly in its entirety through one or more `modules`. So even if only a fraction of these components may be needed by the customer, we would have to pre-load all of them, which is an unnecessary waste of resources.

So what are we talking about here? How about a way where we load only those components out of our library which are required by the customer? This is where the concept of code-splitting comes into use.

Using code-splitting, we conditionally import the required components at run-time.



The Setup

Let's see how to go about it. I have created a few simple custom angular components, which are to be conditionally loaded. So, as expected, these components are NOT part of any application module.

We need to remember that the entire operation is driven by CMS data. So we need some mechanism to evaluate this data at run-time to determine the components.

So we would need a custom structual directive for this. This directive would dynamically create the component instances and inject them into the DOM. Let's first look at the HTML needed at the implementation side, using this directive:


Now, let's take a look at the custom structural directive:


The most interesting bit is the import statement (line 25). Based on the CMS content, I only import that particular module and component. This is a huge advantage in terms of app-performance, since we now do not have to load a module unless it is explicitly asked for, much like a lazily loaded route!
When I place this directive on a designated location in the DOM (the ng-container in this case), I get access to that location's reference, through ViewContainerRef. I make use of angular's ComponentFactoryResolver API to build an instance of our required component and the viewContainerRef injects this component instance at that designated spot.

A small, but very important, GOTCHA!

Since our custom components are not included in any module, or any eager import statements, the angular compiler will exclude these **.ts** files from compilation. To forcefully compile them, so that they can be used at run-time, we need to tell `tsconfig.app.json` to include these files.


Cheers!

Thursday 1 July 2021

Faster angular universal with pwa

This article speaks about an interesting side-effect of mixing angular universal with progressive web application (pwa) - and that's a massive performance boost!

The Setup

--
Let's start by creating a standard angular-cli application (v12.1.0 in my case). I enable SSR (angular universal) on that.

universal

Once this is done, let's quickly check if SSR is working as per our expectation.

run universal

Node server fires up on port 4000 and I check for my webpage's source.

pre view-source

Wonderful! Nothing fancy so far, and everything works great! CLI application works and universal works.

The next step is to add PWA support.

pwa

No extra configurations so far, and that's how it will be. Let's build our universal application once again and serve it on localhost:4000.

But wait!!! Now when I view my webpage source on the browser, I get this:

post view-source


So does this mean that my SSR is broken?

The answer is Nope! My SSR is perfectly fine :-) Let's run a quick check to prove this. Open up POSTMAN and run a GET request against http://localhost:4000. And voila! I see my server-side-rendered page once again!

postman


So what is happening here?

The conclusion and explanation

--
The reason why we do not see the server-rendered content on the browser source is that, the index.html is now cached by service-worker at the browser. The browser does not need to wait for the server to render the content any more, and it simply serves the cached version, and Angular takes over thereafter, like a normal SPA. Open up ngsw-config.json and verify that index.html is one of the cached resources.

ngsw-config.json


The search-engines/crawlers, on the other hand, would see your website just the way POSTMAN sees it, and will continue to have the entire generated HTML for search-engine-optimization. So this way, you gain the performance boost via service-worker caching, without losing the advantages of angular universal!

Note (and a small exercise): If you remove index.html from ngsw-config.json and re-build your universal, you would see that the server-rendered content is back on your browser source, since the HTML is now no longer being cached!

Cheers -:)

Sunday 27 June 2021

Angular scroll-to-top implementation

@angular/cli: 15.2.8
Source: GitHub

In this article I would like to talk about an often-used feature on a user-centric application - the scroll-to-top button for allowing a user to quickly scroll to the top of the page, without having to use the mouse. The end result would be something like this:




Let's break up this implementation into 3 steps:

1) Set up the scroll-to-top button (with some nice styling).
2) Determine when the button should be displayed.
3) Scroll to the top of the page on clicking the button.

Setting up the scroll-to-top button:
I have used @angular/material for some easy styling. In order to display the button fixed to the bottom-right corner of the page (and some nice hover effects with opacity), I have used the below properties on the :host -

scroll.component.scss

Determine when the button should be displayed:
This is an interesting section. To begin with, we need to set up an Observable on the scroll event of the DOCUMENT. You may directly access the document object, or you may use an InjectionToken<Document>, provided by angular.

This observable starts emitting values as soon as the user starts to scroll up or down. Now we are not really interested in the actual scroll event, but we would like to check something important every time the scroll event is fired - and that is the scroll position. Angular makes this quite simple with the ViewportScroller service from @angular/common package. This service has few useful methods, and I am going to use two of them - getScrollPosition and scrollToPosition.

list.component.ts

The getScrollPosition method returns an array of 2 numbers - the X and Y coordinate. I check if the Y coordinate is more than 0 (which means the user has scrolled down) and I enable to scroll-to-top button.

Scroll to the top of the page on clicking the button:
For this last part, I make use of the scrollToPosition method, which again takes in the X and Y coordinates, and I scroll to [0, 0].

list.component.ts


That is all. Hope this was helpful :)

UPDATE:

The source code has been updated to angular 15, with standalone components. The main branch contains the updated syntax of angular 15. Refer to the legacy branch to view the old syntax of angular 13.

Cheers!

Sunday 25 April 2021

Angular infinite scrolling using RxJs and NgRx

@angular/cli: 12.1.0
Source: GitHub

I have seen multiple blog posts on this topic which make use of one method or the other. I just thought of putting everything together under one hood - RxJS, NgRx, Angular Material and in the end, pure document property access. This is what is leads to:

preview


Let's quickly talk about the setup here - I am using an Angular CLI 12.1.0 project. I have NgRx state management set up, with a root state and a feature state. Of course, NgRx is in no way related to the purpose of this application, but I just thought it would not hurt to implement it. We have to admit, as your application grows in size, NgRx does give you some peace of mind :-)

The UI is built using angular material. I have used the mat-list and and the mat-spinner components. I have some hard-coded dummy data, which I have converted into an observable stream, with a delay of 1500 ms, which gives some feeling of having fetched data from a REST service.

The key logic is to calculate when the user has scrolled down to the edge of the page. I chose a simple formula derived from the document object. Of course there are other fancy approaches like using an IntersectionObserver, but that's something I felt was too much for me!

data.service.ts

The above formula works great on a desktop, as well as a mobile (you can trust me, I have tested it myself - but feel free to drop me a note if it doesn't).

I set up the scroll action using RxJS fromEvent.

data.service.ts

Once the scroll event is set up, whenever the above formula is satisfied, the next set of data is called for. For my sample use case, I have used a limit and skip value, which I have used to slice off a chunk from my dummy data set. Every time a new set is emitted by my data observable, I have made use of a reducer function to append it to my previous list of data.

feature.reducer.ts

This is where I store the next limit and skip values for the next data load, and also make a simple calculation of whether my data load is complete, and set it on my state. Once this flag becomes true, subsequent scroll actions do not trigger any more data fetch.

scroll.component.ts

Cheers!

Thursday 12 November 2020

Angular timeout and error handling with RxJS

@angular/cli: 8.3.23
Source: GitHub

This is a short article which demonstrates how we can leverage RxJS to effectively set a time-out on long running server requests, and then display a meaningful error message when that happens.

For demonstration purpose, I have made use of Angular in-memory-web-api for REST services (Refer to in-memory-web-api).

I have set up text-box and a button, and I would enter a specific item-id to retrieve the corresponding item. For the in-memory-web-api, I have set a delay of 500ms.



If I now add a timeout of 250ms (timeout imported from rxjs/operators), the next button click to call the web-api would time out. Note that, since the button click is tied to an action stream, any error thrown would effectively complete the observable.



For handling this error, my error-handler method checks for the error instance, and it comes as an instance of TimeoutError (also part of rxjs).





Please note that effective handling really depends on the structure of your REST API.

Now comment out the timeout and try fetching with a blank id. The request is again errored out, and effectively handled in my error-handler method.


Try changing the URL from 'api/items' to 'api/item'. The request errors out with a different error, but handled through the same logic.


Cheers!

Saturday 5 September 2020

Autocomplete with RxJS and Angular Material

@angular/cli - 15.2.8
@angular/material - 15.2.9

Source: GitHub

This article talks about an efficient way to implement auto-complete feature using Angular concepts. The key components used in this demonstration are as follows:

1. Angular Material (v15) Autocomplete module.
2. RxJS for action and data binding.

I have used Dummy-JSON for getting fake JSON data.






Let us now see how we go about it.


The UI:

The front-end consists of an angular autocomplete module. The input event taps the user's keystrokes and emits a new value into an action subject.


Each key-stroke emits a new value into an action subject declared in my ProductService class.




The Service:

I selected a Subject, since I want it emit a value only when the user starts to type. The action$ is an observable built out of this subject.



Every time a new value is emitted into the action stream, it returns an observable from the http GET request. Since this is a higher-order observable (i.e., an observable returning an observable), we make use of one of the higher order functions like switchMap.


So why switchMap (and not other higher order functions like concatMap or mergeMap)? The reason is: switchMap simply unsubscribes from the previous observable once a new value is emitted by the parent observable.

What this means is - as soon as the user types in another letter or removes a letter, there is simply no need to subscribe and execute the rest call for the previous values. The user is only interested to see the results of the search according to his/her current input. And switchMap does this automatically, and thus gets rid of unwanted rest calls.

We can also get additional control on when to fire the rest service depending on the length of the user's input. For example, in this demo, I fire the rest call only when the user has entered at least 2 characters. If not, then I return an observable of empty array (using of([])).

Use distinctUntilChanged operator to exclude user inputs which have not changed during the debounce period. So the http call will only fire for new input values, and not if the user quickly changed the input and reverted to the old value.


Back to the Component:

We tap onto the above observable and use async pipe to subscribe to it. Since we are completely using observables, we can also use the OnPush change-detection-strategy.


And that is all. You can download the source code from GitHub and try it on.

Cheers!