Card image cap

Working with Navigation in Angular and Lazyloading Modules

Angular  • Posted 2 months ago

So far we have been looking at various building blocks of an Angular application such as Forms, Authentication, making HTTP requests, Observables and so on. In this article, let's look at another important block in developing truly interactive applications - Navigation.

Getting started - What is Navigation?

In any user interactive application - be it a Web application or a Mobile, one needs to move across screens which hold a single feature of the application. One can't fit in all the features and interactions required in an application in a single View for the user, isn't it? Instead, we separate all the features offered in an application into different Views or Screens and let user move around between the Views or screens which constitutes to the user's experience. This moving around between the screens or views in the application is what we call as "Navigation". And the triggers for this Navigation are called as "Routes".

A route is simply "path" to a particular view or screen which offers some value or feature to the user. Be it an individual BlogPost bearing content for the user to read, a signup form for the user to register as a member of the application, and so on. When you look at it, the landing view of the application which is shown when the user opens the application for the first time is also a "route" configured to show the landing view. Got the idea right?

Routing in Angular - RouterModule:

We know that Angular is a framework to develop Single Page Applications, which means that the entire application we develop using Angular runs only in a single HTML page with regards to the server where this application is hosted. There should be a mechanism to keep track of the route changes and serve the user with views without having to create a new server request for new page. In Angular, the navigation between the Views based on the user interaction is handled by the RouterModule.

Generally, when the URL in the browser changes, a new request is triggered to the server to load the requested new HTML page. The browser's location and history are updated to the new URL. But since Angular application runs entirely on the client browser, the RouterModule ensures that there no server request raised when the route changes, and instead handles the route changes whenever one happens.

This technique is used in HTML5 browsers called as "HTML5 pushState" style which is handled by the RouterModule using its "PathLocationStrategy".

Configuring Routes:

Like all other Modules we have seen so far such as the FormsModule, HttpClientModule and ReactiveFormsModule; RouterModule also needs to be imported into the Angular AppModule. Along side, we should also need to provide the RouterModule with information about all the routes available in our application and should configure it with instructions about which Component needs to be invoked when a particular route is selected by the user to navigate to.

We build Routing and Navigation into our Angular application in three simple steps:

  1. Importing the RoutingModule into our AppModule
  2. Configuring the Routes and related Component information
  3. Adding the Router-Outlet directive - which specifies where this Component invoked by the selected Route needs to be rendered.

Explaining with an Example:

Let's go back to our example of the OpenSocialApp we have been building as a part of our journey.

The OpenSocialApp contains two routes:

  • User specific routes which show features related to the User such as User Profile and so on.
  • Post specific routes which show features related to the Posts created by users such as a Single Post page, Posts List page and so on.

Let's create the routing table for these features and look at how these work. As per the step 1, we include the RoutingModule inside our AppModule imports.

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent
  ],
  imports: [
    BrowserModule,
    PostsModule,
    AuthModule,
    RouterModule.forRoot(routes) <-- Here
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    multi: true,
    useClass: ErrorInterceptor
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }

Observe that we're also passing a variable routes which is of type Routes, to the RoutingModule. This routes array contains the paths and the components to be loaded for those paths, which we shall configure in step 2.

For the Step 2, we need to create an array of Route objects, which looks something like this:

const routes: Routes = [
    { path: "posts", component: PostListComponent, canActivate: [AuthGuard] },
    { path: "posts/:id", component: PostItemComponent },
    { path: "", component: PostListComponent, canActivate: [AuthGuard] },
    { path: "**", component: PostNotFoundComponent }
]

Each Route object takes in several properties, important among them are:

  • path - the relative URL Path on which this Route is to be matched
  • component - the Component that needs to be loaded when this Route is matched
  • canActivate - a set of validators which need to be satisfied by the request before this Route can be loaded

The sample route array shown above belongs to the Posts features in the application. It contains the various paths which may occur in Posts section and the respective components to be loaded when a particular post related route is matched.

For example, when a User opens the application, the very first component loaded shall be the PostListComponent because it matches the "empty path" that a typical landing page path "/" would have.

Similarly, let's say the user clicks on a post with the link "/posts/show-me-this-post", the second route path "posts/:id" is chosen, since the clicked link contains a route param "show-me-this-post" which fits the ":id" route parameter in the route.

The last in the routes array is the "**" path which is also known as a "wildcard" route. This route is chosen by the RoutingModule when no other route in the RoutingTable matches the input path. That's why it makes sense to put a PostNotFoundComponent view which is loaded when no other route in the Posts matches the URL.

Finally the step 3, where we add a tag inside our HTML view. This generally resides in the app.component.html, which is a sort of entry point for the application. This is actually a directive which defines where the component matching the route must be loaded inside the View.

Our app.component.html looks like this:

<app-navbar></app-navbar>
<div class="container-fluid">
    <router-outlet></router-outlet>
</div>

Configuring RoutingTable - Best Practices:

If you look at the sample routes array, you can observe that there's a pattern in which we've intentionally added the routes into the array.

  • In Angular, the RoutingModule follows the "first-match" approach which means that whatever route matches the input path is picked up "first".
  • Wildcards by their nature are matched for all routes and hence they need to be placed last in the RoutingTable.
  • The "empty route" is placed before the "wildcard" but at the end of the Routes.
  • The RoutingTable starts with generic routes such as "/posts" which are a sort of landing pages for the section followed by the more specific routes such as "/posts/:id" which represent a single item of the list of entities.

Lazy Loading Modules based on Routes:

We know that we have two sections in our application:

  • Users and
  • Posts

In Angular we can separate the dependencies and features from the AppModule into separate Module classes which encapsulate all the dependencies and functionalities of a particular feature for better separation of concerns, which we shall look into in a different article.

In our case,

  • the routes and components related to Posts can be used inside the PostsModule, which is referenced inside the AppModule.
  • Same goes with the User related routes and components can be placed inside a separate module called AuthModule.

Generally, Modules in Angular are loaded "eagerly", which means that all the Modules are loaded into the application even when at the moment some of the modules are not needed.

In the AppModule, we need to somehow specify to the RoutingModule that the routes related to "posts" are available inside the PostsModule and the Module needs to be loaded for the routes. Same goes with the routes related to "users" as well, which need to be looked into the AuthModule.

Now we don't want to load all these routes beforehand into the RoutingModule at the AppModule, since its a waste to already have all the routes related to "user" be loaded upfront, when all a user does is navigating inside the Posts.

For this, we can improve efficiency by loading the Modules only when "actually" needed. This we call as Lazy Loading. In the AppModule where we need to load the RoutingModule with the initial routes, we specify the routes as below:

const routes: Routes = [
  { path: "posts", loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule) },
  { path: "auth", loadChildren: () => import("./auth/auth.module").then(m => m.AuthModule) },
  { path: "", loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule) },
  { path: "**", component: NotfoundComponent }
];

Observe the syntax,

{ path: "posts", loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule) }

The Route object also contains a loadChildren property which accepts a function callback. In this we import the "PostsModule" class and then point to the PostsModule. The PostsModule is configured to use the PostsRoutingModule which looks like below, where we've configured the RoutingModule with necessary routes inside the Posts module:

import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router';
import { PostListComponent } from './post-list/post-list.component';
import { PostItemComponent } from './post-item/post-item.component';
import { AuthGuard } from '../auth/auth.guard';

const routes: Routes = [
    { path: "posts", component: PostListComponent, canActivate: [AuthGuard] },
    { path: "posts/:id", component: PostItemComponent },
    { path: "", component: PostListComponent, canActivate: [AuthGuard] }
]

@NgModule({
    imports: [
        RouterModule.forChild(routes),
    ],
    exports: [
        RouterModule
    ]
})
export class PostsRoutingModule { }

This PostsRoutingModule needs to be imported inside the PostsModule, so as to call this Module when the application starts up.

@NgModule({
    declarations: [
        PostItemComponent,
        PostListComponent,
        NewPostComponent
    ],
    imports: [
        CommonModule,
        PostsRoutingModule, <-- imported here
        FormsModule,
        HttpClientModule
    ],
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: PostsInterceptor,
            multi: true
        }
    ]
})
export class PostsModule { }

The AppModule no longer contains imports for PostsModule or AuthModule, and instead these modules are loaded only when the respective routes are matched.

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes),
    HttpClientModule
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    multi: true,
    useClass: ErrorInterceptor
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }

Adding Navigation Links in Views:

So far we've looked in detail about configuring routes and adding paths and so on. Now the next question is, how do we use these? The answer is: routerLink.

routerLink is another directive from the '@angular/router' that allows us to create route links inside our Views. For example, in our PostListComponent, we're showing a list of Posts we fetch from the API. We want to navigate user to a single post view, when the user taps on any post card in the list. To do this, we need:

  • The path to which the user shall be navigated to - let's say it is PostItemComponent
  • The id of the post which the user has tapped, so that in the PostItemComponent we shall fetch the data related to that particular post and show it to the user.

For this, we make use of the routerLink directive on an anchor tag which invokes the router for the change in path, when the user taps on this link.

<div *ngIf="isLoggedIn">
    <app-new-post></app-new-post>
</div>
<!-- routerLink with array params is called link parameters array -->
<a *ngFor="let post of posts;" [routerLink]="['/posts', post?.id]">
    <div class="card my-2">
        <div class="card-body">
            <div class="container-fluid">
                <h5 class="card-heading">@{{post?.postedBy}}</h5>
                <p class="card-text">{{post?.text}}</p>
            </div>
        </div>
    </div>
</a>

Observe how we are passing the postId param to the routerLink. This array representation of passing various parameters into the routerLink is called "link parameters array".

[routerLink]="['/posts', post?.id]"

Here we're passing the path which is "/posts" and then we're adding a router parameter representing the Id of the tapped post object.

When user taps on this:

  1. RouterModule at the AppModule is invoked - it checks for the path "/posts/12345", this is routed into the PostsModule
  2. Inside the PostsModule which has the PostsRoutingModule imported, the route "/posts/12345" matches with "/posts/:id" where id is a route parameter. In this case it is 12345
  3. The mapped component is PostItemComponent which the RouterModule decides to load.

Inside the PostItemComponent, we need to pull this id value thus matched and stored inside the Router to be able to fetch data for that particular postId.

To pass query parameters in this setup, we add another directive queryParams, which enables us to pass query string along with route parameters.

<a *ngFor="let post of posts;" [routerLink]="['/posts', post?.id]" [queryParams]="{timestamp:'abcdefg'}">
    --- card ---
</a>

Reading Data from the Route:

To be able to read data from the current route, we make use of the ActivatedRoute service which is from '@angular/router' module. This service instance contains detail about the route such as route parameters, query parameters and so on.

The PostItemComponent looks like below:

import { Component, OnInit } from '@angular/core';
import { Post } from '../post';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { PostsService } from '../posts.service';


@Component({
  selector: 'app-post-item',
  templateUrl: './post-item.component.html',
  styleUrls: ['./post-item.component.css']
})
export class PostItemComponent implements OnInit {
  post: Post;
  constructor(private route: ActivatedRoute, private postService: PostsService) { }
  
  ngOnInit(): void {
    this.route.paramMap.subscribe((params: ParamMap) => {
      var postId = params.get("id");
      console.log(postId);
      this.postService.getPost(postId).subscribe((post) => {
        this.post = post;
      });
    });
    this.route.queryParamMap.subscribe((params: ParamMap) => {
      var postId = params.get("timestamp");
      console.log(postId);
    });
  }
}

Here we're injecting the ActivatedRoute service via the constructor and then calling the ActivatedRoute.paramMap which returns an Observable. When the params are available, we obtain the router parameter value that is matched to "id", and then our logic continues.

this.route.paramMap.subscribe((params: ParamMap) => {
    var postId = params.get("id");
    --- fetch data ---
});

If we want to create a navigation from our component class instead of a routerLink. we use the Router service from '@angular/router' through which we can create a similar navigation call.

back() {
    this.router.navigate(['/posts'], {
      queryParams: { timestamp: 'abcdefg' } <- queryString if want to pass
    });
  }

Securing Routes with Guards:

Preventing unwanted access to a route is equally important as Providing access to a route. Back when we were configuring our RoutingTable, we were adding something called as "canActivate", remember? This canActivate is first triggered for routes which are configured when one tries to access that route. This canActivate contains a series of Guard classes which decide whether to allow access to a route or not to allow access.

In our previous article where we were looking at Authenticating users with IdentityServer4, we first used the AuthGuard to allow or deny access to PostList component for users who have not logged into the application. When we look back into the AuthGuard we've created for it, we were configuring the AuthGuard into our Routes there!

import { CanActivate } from '@angular/router';

export class AuthGuard implements CanActivate {
    canActivate(): boolean {
        -- some logic which returns a boolean --
    }
}

This is a Guard class which implements the CanActivate interface. This is what we pass into the canActivate property of the route object. Now it makes sense right?

{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] }

Final words:

Angular router is a powerful means of providing navigation across components in an application, all inside the client browser. When you look at the Views, one can observe that nowhere we've used the traditional "href" tags for the tags, and instead use the routerLink directive for routing. Otherwise it creates a server request, which is something not desired in single page applications.

On the other hand, LazyLoading the modules decreases the initial page load time which accounts to a faster app experience. Since the modules are not called unless required it positively impacts the application memory as well.

How do you detect browser in JavaScript / jQuery?
How can you protect your JS files in angular?
How would you implement a form having two components where one component updates another?
How can two components communicate with each other?
We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept