Loading...

Angular - Routing and lazy loading

Routing in angular

a major part of any web application is how we deal with routing. Change in the route should display a different screen for the user or a part of the current screen should change. When user reloads the page, the app should return him to the exact view based on the url he is in.
When creating a Single page application, the routing should happend with HTML5 history push state, meaning the browser should not perform a reload when doing the navigation from our angular app, and of course although we are using the history push state to navigate we still have to support the back button as well.
This cause the navigation to be really quick. Dealling properly with urls has a major impact on Search Engine Optimization (SEO) as well, so there are certain rules we have to follow.

  • App resources - First thing you have to do is define what is a resource in your app, I like to call a resource as a certain page in your app, you will have many different pages like that, the info for that page is usually grabbed from a table in a database, and the page looks similar between every resource only the content of the page change and not the general layout and appearance.
    For example a resource in StackOverflow, might be a question page, a resource in a wordpress application, might be a single article.
    If it's important for us that every resource will get good SEO, the url has to be in this format: /<topic>/<slug> where topic is the section of this resource, so in StackOverflow it will be question and in wordpress it might be article
    slug is the title of the resource where words are seperated with a dash.
    that is the case if your slug is unique and you can identify the resource with just a slug. If that is not the case you will have to append the primary key as well like so: /<topic>/<pk>/<slug> remember to also be consistent so if your urls do not end with a slash then all of them should be like that.
  • Query params - query params will be added in cases like when we are doing a search in our app, or filtering the search result. The crawl bots will not give the query params any SEO value.
Let's see how we can deal with routing in our angular app.

@angular/router

@angular/router package contains the module that contains the router and routing logic. You will need to install it with npm

        
            > npm install @angular/router --save
        
    

If you start an angular cli project this package is already installed.

RouterModule

If we want to use router module services or directive we have to include it in the imports array, we will also need to consider now if our module contains routing.
Since we are breaking our angular app to modules, one root module and multiple feature modules, we will have to break our app routing to modules as well.
For example say we want to add a setting screens in our app, we have a user setting, account setting, and dashboard settings.
We decided our settings url to look like this:

  • user settings - /settings/user
  • account settings - /settings/account
  • dashboard settings - /settings/dashboard
Looking at things from a modular stand point, it's highly likely that you would like to place every setting related things in a SettingsModule and that module will have a routing definition of it's own with the above urls. So the routing definition for the settings module will be in a module called SettingsRoutesModule this will allow us to keep our app consistent with the modular paradigm and also maintain lazy loading which we will cover later in this article. Let's create an app with a homepage and an about page in the root module, and create settings section located in the settings module.

create a new module containing the routing for our root module

        
            > ng g module AppRouting
        
    

Also let's create a component for the homepage and a component for the about page

        
            > ng g c HomePage
            > ng g c AboutPage
        
    

Let's define our root module routing in the app-routing.module.ts

        
            import { NgModule } from '@angular/core';
            import { RouterModule } from '@angular/router';
            import { HomePageComponent } from '../home-page/home-page.component';
            import { AboutPageComponent } from '../about-page/about-page.component';

            @NgModule({
            imports: [
                RouterModule.forRoot([
                { path: '', component: HomePageComponent},
                { path: 'about', component: AboutPageComponent},
                ])
            ],
            exports: [RouterModule]
            })
            export class AppRoutingModule { }
        
    

A few things to note here

  • There is no components in the routing module, the components belong to the root module, the routing module will contain only routes configurations and service related to routing like can activate guards.
  • We import the router module and since the app routing will describe the routing for the root module, we use the RouterModule.forRoot we will call the forRoot method only once. The forRoot will get an array with routes configurations and for now we are just keeping each route simple, we are matching a component to a path.
    Notice that we are also passing the RouterModule in the exports array, the reason for that is that module which adds the routing module won't have to include also the RouterModule to have access to all the directives and services of the router module.
    Let's modify the root module to include the module we just created. app.module.ts
        
            ...
            imports: [
                BrowserModule,
                AppRoutingModule
            ],
            ...
        
    

Our routing for the app module is installed, but where will the router place the components after a route match?

router-outlet directive

The router will place the matched component to path inside the router outlet directive. Modify the root component template app.component.html to contain the router outlet directive.

        
            <nav>
                <h1>
                  This will be shared in all the routes
                </h1>
            </nav>
              
            <router-outlet></router-outlet>
        
    

So the router outlet directive is a placeholder that tells the router where to place the matched component.
a router outlet can also be associated with a name, there can be more than a single router outlet. How exactly does it work with multiple router outlets.
we can place attribute in the router oulet directive called name, there can be only one unnamed router outlet which is called the primary outlet, and all the rest have to be named.
Let's create another router outlet named messages whose goal is to display a message. In the app.component.html add the following at the bottom of the page

        
                <router-outlet name="message"></router-outlet>
        
    

Create a new component which goal is to display a message to the user

        
            > ng g c Message
        
    

now modify the app-routing.module.ts to contain our new route in the routes array

        
            ...
                { path: 'message', component: MessageComponent, outlet: 'secondery'},
            ...
        
    

Let's add a link in the app component that will pop the message in the outlet.
modify the app.component.html

        
            <nav>
                <h1>
                  This will be shared in all the routes
                </h1>
                <ul>
                  <li>
                    <a [routerLink]="[{outlets: {secondery: ['message']}}]">
                      pop message
                    </a>
                  </li>
                </ul>
            </nav>
              
            <router-outlet></router-outlet>
            <router-outlet name="secondery"></router-outlet>
        
    

So to link to the named outlet we have to place a url in a specific outlet, so we place a dictionary with outlets key and the name of the outlet and the url of the navigation parts for that outlet.

Lazy loading

Another awesome feature of angular routing is the built in lazy loading. Let's create our settings module which contain routes for user, dashboard, and account settings.
We know that our users won't access the settings module every time they activate the app, in fact most of them will just visit the settings once. For this reason we don't want to load all the code for the settings module every time the user loads our app, we want to load the settings module only when the user wants to access the setting pages.
Let's create our settings module, settings-routing module, account component, dashboard component, and user component that are part of the settings module.

        
            > ng g module Settings --project routing-tutorial
            > cd projects/routing-tutorial/src/app/settings/
            > ng g module SettingsRouting
            > ng g c Account
            > ng g c User
            > ng g c Dashboard
        
    

Modify the SettingsRouting module to contain the routes for the settings module.

        
                import { NgModule } from '@angular/core';
                import { CommonModule } from '@angular/common';
                import { RouterModule } from '@angular/router';
                import { UserComponent } from '../user/user.component';
                import { AccountComponent } from '../account/account.component';
                import { DashboardComponent } from '../dashboard/dashboard.component';
                
                @NgModule({
                  imports: [
                    CommonModule,
                    RouterModule.forChild([
                      {path: 'user', component: UserComponent},
                      {path: 'account', component: AccountComponent},
                      {path: 'dashboard', component: DashboardComponent},
                    ])
                  ],
                  exports: [RouterModule]
                })
                export class SettingsRoutingModule { }
        
    

Notice that for this module we created the routes using the forChild method, so every feature module we are creating routes for will have the routing defined with forChild notice that although we plan the user settings url to be /settings/user we still only placed in path the user and removed the common prefix from all the routes here.
We have to also modify the SettingsModule to include the settings routings module we just created.

        
                import { NgModule } from '@angular/core';
                import { CommonModule } from '@angular/common';
                import { AccountComponent } from './account/account.component';
                import { UserComponent } from './user/user.component';
                import { DashboardComponent } from './dashboard/dashboard.component';
                import { SettingsRoutingModule } from './settings-routing/settings-routing.module';
                
                @NgModule({
                  imports: [
                    CommonModule,
                    SettingsRoutingModule
                  ],
                  declarations: [AccountComponent, UserComponent, DashboardComponent]
                })
                export class SettingsModule { }
        
    

Now in the root routing of our app we need to tell the router that when we are grabbing a url with prefix /settings load the settings module and the routing there will take control.
Modify the app-routing.module.ts to look like this:

        
                { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule'}
        
    

Notice that the loadChildren is refrencing the file of the lazy loaded module as well as the name of the class.
Try and restart the ng serve, you should see a new file for the settings module is created, let's add a link in the app.component.html modify it to look like this:

        
            <li>
                <a routerLink="/settings/user">
                  user settings
                </a>
            </li>
            <li>
                <a routerLink="/settings/account">
                  account settings
                </a>
            </li>
            <li>
                <a routerLink="/settings/dashboard">
                  dashboard settings
                </a>
            </li>
        
    

try and click any of the new links and notice how the new created files for the settings module is loaded only when you are pressing the links

ActivatedRoute - passing params

The last topic we want to cover is passing data from the url.
We can pass data with the url in two ways:

  • Matrix params - section of our url is dedicated for dynamic params for example: /questions/134523/ the second section of the route signifies the primary key of the question we are looking for, we will call this way of transfering data as matrix param
  • Query params - key value at the end of the url, query params do not affect the router match while matrix param the param is defined in the url.

Let's create another module called TodoModule with a todo routing module and a component page for a list of todos and a single todo details page

        
            > ng g module Todo --project routing-tutorial
            > cd projects/routing-tutorial/src/app/todo
            > ng g module TodoRouting
            > ng g c TodoList
            > ng g c TodoDetails
        
    

Modify the todo routing module and add routes for the two components. todo-routing.module.ts

        
                import { NgModule } from '@angular/core';
                import { CommonModule } from '@angular/common';
                import { RouterModule } from '@angular/router';
                import { TodoListComponent } from '../todo-list/todo-list.component';
                import { TodoDetailsComponent } from '../todo-details/todo-details.component';
                
                @NgModule({
                  imports: [
                    CommonModule,
                    RouterModule.forChild([
                      {path: '', component: TodoListComponent},
                      {path: ':id', component: TodoDetailsComponent},
                    ])
                  ],
                  exports: [RouterModule]
                })
                export class TodoRoutingModule { }
        
    

Notice that for the details route we are specifying that we are getting a matrix param called id.
In our todo details component we will want to access our param. Modify the todo-details.component.ts

        
                import { Component, OnInit } from '@angular/core';
                import { ActivatedRoute, ParamMap } from '@angular/router';
                import { map, mergeMap } from 'rxjs/operators';
                import { Observable } from 'rxjs';
                import { HttpClient } from '@angular/common/http';
                
                
                @Component({
                  selector: 'app-todo-details',
                  templateUrl: './todo-details.component.html',
                  styleUrls: ['./todo-details.component.css']
                })
                export class TodoDetailsComponent implements OnInit {
                  todoItem$: Observable<any>;
                
                  constructor(private _activatedRoute: ActivatedRoute, private _httpClient: HttpClient) { }
                
                  ngOnInit() {
                    this.todoItem$ = this._activatedRoute.paramMap.pipe(
                      map((params: ParamMap) => params.get('id')),
                      mergeMap((id: string) => this._httpClient.get('https://nztodo.herokuapp.com/api/task/' + id + '/?format=json'))
                    )
                  }
                
                }
                
        
    

Few things to note here, we are injecting the ActivatedRoute which contains detail about the current state of the router and the current match that the router found, among the data is the query params and matrix params the route contains.
We are getting the params from the activated route in an observable paramMap we do some rxjs operators magic to transform the data we get to an ajax call to the todo resource based on the id we are getting in the params.
We still need to connect the final todo routing to the todo module and also include in the imports the http client module. We will leave that for you folks back at home.

Summary

So in this article we played a bit with angular routing, we save how we can create modular routing, lazy load our modules, create multiple router-outlet and pass data with the url.