Lazy load modals with Angular! šŸ˜“

Parham
Code道

--

By default, NgModules are eagerly loaded, which means that as soon as the app loads, so do all the NgModules, whether or not they are immediately necessary. For large apps with lots of routes, consider lazy loading ā€” a design pattern that loads NgModules as needed. Lazy loading helps keep initial bundle sizes smaller, which in turn helps decrease load times.
https://angular.io/guide/lazy-loading-ngmodules

To lazy load a feature module you can simply define a route for it and thatā€™s it!
Here is an example to remind you how itā€™s done.

const routes: Routes = [
{
path: 'map',
loadChildren: () => import('./map/map.module').then(m => m.MapModule)
}
];

Now Angular CLI will separately bundle map module and it will only be loaded when you access the /map route.

What about modules used in the map that do not have a separate route?
Like modals or any piece of UI component that is not visible in view initially and will appear based on different workflows.

By default, all these modules will be loaded with the map module bundle, even though if the user never uses these features.

Lazy load feature modules in an already lazy-loaded feature module

In my application, I have a map view which is a feature module with the route /mapthat hosts a bunch of other features.

  • Search modals that will only be visible when the user is searching on the map.
  • Layer selectors to find a layer to add to map which is collapsible. So it will be only visible when expanded.
  • Layer details which are only visible when a user clicks on a layer in the side panel to see its details.
  • Also, some more UI pieces that will be visible when the user selects a specific menu. Like Draw controls, Print form, Set Region control and more.
  • Plus each one of these UIs utilises services and other dependencies under the hood which can quickly add up to map module bundle size.
Lazy loaded search result modal in invest.agriculture.vic.gov.au
Lazy loaded layer selector in invest.agriculture.vic.gov.au
Lazy loaded draw panel in invest.agriculture.vic.gov.au

I want to load these pieces of UI only if the user needs them.

Obviously, I can lazy load the other feature modules like map module but the caveat is that accessing routes that define other feature module means leaving and unloading the map which is not going to work for my use-case.
For example, I need to stay on the map and show the modal.
So I need to lazy load feature modules in an already lazy-loaded mapmodule.

This will help me to reduce the initial bundle size of the /map module to make loading of the map faster.

So the question is:
How to lazy load those feature modules without leaving the /map route?

Angular Auxiliary Routes to the rescue

Angular Auxiliary Routes let you lazy load parts of the view that are not visible and bundle those separately.

The following video shows how invest.agriculture.vic.gov.au uses this capability. Here I am starting in the context of /map route and all the JS required for the visible part of the view is loaded with the map module.
Pay attention to network requests when I open the search results modal. Itā€™s only then that Angular loads the JS files required for the SearchModule and my modal.

Lazy loaded search result modal in invest.agriculture.vic.gov.au

invest.agriculture.vic.gov.au is too complicated for the learning purpose so I made an Example application that replicates the lazy-loaded modal and map.
This application is available on Github. So you can download and follow the same steps mentioned in the rest of my article.

Here is a demo of the final result.

Letā€™s see what is happening with the URL here

This is the syntax for the URL
http://base-path/#/primary-route-path/(outlet-name:route-path)

http://localhost:4200/#/map/(map-outlet:modal)
  • http://localhost:4200 is the base path. I am using HashLocationStrategy which is why I have /#/ after base URL.
  • /map is the primary path for the map view.
  • The remaining part in the parenthesis is my auxiliary route (route within the /map route) (map-outlet:modal)

Letā€™s breakdown the auxiliary route

  • ( open and ) close parenthesis indicates the beginning and end of my auxiliary route.
  • Followed by map-outlet which is my outlet name. This is the name I selected for my secondary outlet. I will explain this further shortly.
  • Followed by : that separates the outlet name from the actual path.
    In this case, the path is modal for modal. I am using short names here to shorten my URL length but you can use any name.

How to build the auxiliary routes?

First, I need a secondary router outlet (named router outlet).
In Angular, you can have one default(primary) router outlets and as many named(secondary) router outlets as you need.

The RouterOutlet is a directive from the router library that is used like a component. It acts as a placeholder that marks the spot in the template where the router should display the components for that outlet.

As any other Angular have we will have a primary outlet defined in the app.component.html where we render the primary routes.

<ul class="menu">
<li>
<a routerLink="home">Home</a>
</li>
<li>|</li>
<li>
<a routerLink="map">Map</a>
</li>
</ul>

<div class="content">
<router-outlet></router-outlet>
</div>

Here is the top-level application router config defining the /map route, which lazy loads the map module.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
{
path: 'home',
loadChildren: () => import('./home/home.module').then((m) => m.HomeModule),
},
{
path: 'map',
loadChildren: () => import('./map/map.module').then((m) => m.MapModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }

The primary router outlet will render /home and /map. Given the configuration above, when the browser URL for this application becomes, the router matches that URL to the route path /map and displays the MapComponent. The rendered HTML will have the <app-map> as a sibling element to the RouterOutlet that you've placed in the host component's template.

I will place another router outlet in the map.component.html which is a named router outlet (my secondary outlet). Other floating components like modals or sliding panels can use this named router outlet to show within the map context.

<router-outlet name='map-outlet'></router-outlet>

I need to specify the outlet name in the map module router when I define the child routes.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MapComponent } from './map.component';
const routes: Routes = [
{
path: '',
component: MapComponent,
children: [
{
path: 'modal',
outlet: 'map-outlet',
loadChildren: () => import('../modal-wrapper/modal-wrapper.module')
.then((m) => m.ModalWrapperModule),
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MapRoutingModule {
}

Thatā€™s all you need to define a route within a route, and Angular will take care of bundling the lazy-loaded feature modules separately and also lazy loading the feature modules as you navigate to their respective route. Here is the code structure for the map module.

Map feature module structure

Now you can easily use the Angular router to navigate between to Modal route. Take a look at the openModal() method!

map.component.ts

As you can see I am passing multiple commands to navigate method.

Basically telling the router to navigate to /map/(map-outlet:modal) where /map is the map module path, map-outlet is my named outlet and modal is the path for the modal module.

And the last piece of the puzzle to make the router work. Place a <router-outlet></router-outlet> in the map view HTML.

map.component.html

By putting an outlet in the map.component.html I am telling the Angular where to render the content of the named router outlet (map-outlet).

How does the code look like for the modal module?

The architecture for the module is a bit more complicated than the other feature modules in my application since with Angular material dialog I need to pass a component to thethis.dialog.open() method.
Here is how the code structure looks like.

Modal feature module structure

modal-wrapper.component.ts is a wrapper component that will open the modal in the OnInit when the module is loaded by modal route (/map/(map-outlet:modal)).

Plus it will clean up the route to remove the auxiliary route when modal is closed. I am using afterClosed method of Angular Material Dialog to detect when the user closes the modal and change the route to the /map and remove the auxiliary route. OnDestroy will take care of the scenario that user clicks browser navigation back button.
This way I can navigate between modal and map as many time as I want to show/hide the modal.

Here is the code for each file in the modal module.

modal-wrapper.module.ts
modal-wrapper-routing.module.ts
modal-wrapper.component.ts
modal.component.ts
modal.component.html

I have simplified the code here for this demonstration but you can imagine that a feature module can be way more complicated and use more services or other third-party dependencies.

So being able to lazy-load modules will be a great boost to the application performance.

Final notes:

  • As a result of this architecture, you modal is route enabled which means you can refresh the page and Angular will open the modal automatically.
    This is great for bookmarking and sharing of URLs.
    In this case, I can share the link with someone and they can see the search result for the exact lat/long I used for my search.
  • By opening each lazy-loaded route that is defined similarly to the search module on the map I navigate away from the search module route and search modal will get destroyed.
    This can be good or bad based on the use-case.
    In my case, this is exactly what I needed.
    For example, I want to close the search modal when I open the layer selector and vice versa.
    This will naturally happen as I move away from the previous route.
    Without this architecture, you will need to keep some flags locally in map component or on the app state to make sure you close the layer selector component before opening the search modal which can get really messy in a big application.
    Of course, you can use a state management system like NgRX and Redux to help you with managing the state.
  • You can skip the wrapper component (modal-wrapper.component.ts) in your implementation if you have a simpler UI to show/hide.
    The wrapper component is really useful to support the usage of Angular Material Dialog.
  • You can prefetch the lazy-loaded modules using the preloadingStrategy: PreloadAllModules or use a custom preloadingStrategy as described in the following article.

Also if your requirement is just to lazy-load a specific component like a modal and not the whole feature module, you might find the following article useful.

Thanks for reading my article.
I hope you find it useful.

--

--

Parham
Code道
Editor for

Web & mobile developer, @Auth0Ambassador. Follow me for content on JavaScript, Angular, React, Ionic & Capacitor, Progressive web apps & UI/UX.