Routing is an important concept of any web application. We can be glad that the router is a core part of the Angular framework so there are plenty of tools we can easily use. We will talk about the following parts of routing before heading to the challenge.
- Router 1: Router Outlet
- Router 2: Lazy Load
- Router 3: Manual Loading a Module
- Router 4: Guards
- Router 5: Resolver
Branch router/outlet
Most developers have used <router-outlet></router-outlet>
in their careers as part of the routing efforts.
<router-outlet></router-outlet>
This tag provides the anchor point to render any route and its component — they are being inserted after that point in the DOM as a sibling.
Any route that defines child components need to use a distinct router outlet. Don't forget it — otherwise nothing will render and the application won't throw any error.
Branch router/lazy-load
To lazy load a routed module you have to follow a specific syntax in your route in the router configuration.
{
path: 'lazy-module',
loadChildren: './lazy-load/lazy-load.module#LazyLoadModule'
},
See the loadChildren
with the value of? './lazy-load/lazy-load.module#LazyLoadModule'. That's the key to lazy loading. It's the relative path to the module file, and a reference to the Module you want to instantiate in case there are more modules in this file. Angular will do the rest for you! It will create a webpack configuration that will create a separate chunk to load later.
You will see this chunk file appearing in your shell.
Also when you hit the route you will see a file in the format filename-module-name being loaded — which is in this case lazy-load-lazy-load-module.js
. That's the complete module factory Angular created for us. A factory is created by the Angular template compiler and contains templates and services ready to use in the browser once it's loaded.
The module being loaded usually starts with an empty route. This way you give the consumer of your module to decide where module and all of its routes should be mounted. Otherwise it would be hard coded in your module. Of course you can start with any route if your application architecture requires it. The following module would be a valid module that you can lazy load.
const routes: Routes = [{
path: '',
component: HelloComponent
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LazyLoadRoutingModule { }
Lazy loaded modules create their own branch on the Dependency Injection tree and other parts of the application won't get access to provided values (unless you used providedIn: 'root'
and you reference it in your application)
Branch router/manual-lazy-load
Can we load a module manually without the router just to load more features? We need to tell Angular somehow that we want to load a specific module later and that a factory needs to be compiled but not eagerly loaded.
Before loading the module let's create another module to load. We don't want any routing for this demonstration but provide some component we can create dynamically:
@NgModule({
declarations: [AComponent, BComponent, LazyFeatureComponent],
imports: [
CommonModule
],
entryComponents: [
// we already learned, that whenever you want to create a dynamic component
// it must appear in this list.
LazyFeatureComponent,
AComponent,
BComponent
]
})
export class LazyFeatureModule {
static myEntry = LazyFeatureComponent;
static otherComponent = {
a: AComponent,
b: BComponent
};
}
This part of the module is important:
static myEntry = LazyFeatureComponent;
static otherComponent = {
a: AComponent,
b: BComponent
};
You expose the references to your components — so we can access them after loading. There wouldn't be any other possibility to get our hands on the references otherwise. Save and Angular will update the chunk immediately so we can try loading it.
Remember the relative url we just used inside the router must be involved?
./lazy-load/lazy-load.module#LazyLoadModule
You can use such a string (but without the hash followed by the module name ☝️) in your angular.json
config under the key lazyModules
. The schema says about it:
List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.
We will point to our feature module.
"architect": {
"build": {
"options": {
"lazyModules": [
"src/app/lazy-feature/lazy-feature.module#LazyFeatureModule"
]
After restarting the app you should should see your module with a slightly different file name.
You just told Angular CLI about the existence of the module so it can be prepared as a chunk to load later — like a router would have initiated it. Let's load the prepared factory manually. You use the full path provided in the configuration — Angular will figure out the correct file name. Don't try to use the name of the chunk you see in the shell.
That's our loading mechanism.
// tag where we gonna inject the lazyload module and his default component "entry"
@ViewChild('container', { read: ViewContainerRef }) viewRef: ViewContainerRef;
constructor(
private loader: NgModuleFactoryLoader,
private injector: Injector,
private moduleRef: NgModuleRef<any>) {
}
ngOnInit(): void {
this.load();
}
load() {
const path = 'src/app/lazy-feature/lazy-feature.module#LazyFeatureModule';
this.loader.load(path).then((moduleFactory: NgModuleFactory<any>) => {
// myEntryComponent is our property, not part of the framework
const entryComponent = (<any>moduleFactory.moduleType).myEntryComponent;
const moduleRef = moduleFactory.create(this.injector);
const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);
const otherComponent = (<any>moduleFactory.moduleType).otherComponent;
this.viewRef.createComponent(compFactory);
const compFactoryA = moduleRef.componentFactoryResolver.resolveComponentFactory(otherComponent.a);
this.viewRef.createComponent(compFactoryA);
const compFactoryB = moduleRef.componentFactoryResolver.resolveComponentFactory(otherComponent.b);
this.viewRef.createComponent(compFactoryB);
});
}
We use the same module path as in the angular.json
file but in addition we add the module name we want to load. The NgModuleFactoryLoader
will take care of creating the correct url (by convention) to load the factory file. Once it's loaded we can get the reference to the component we want to instantiate. Use the custom static property myEntryComponent
. That way we prevent to include the component reference which would get included in the application module otherwise.
After we get our hands on the component factory we can create instances of all exposed components (A, B, myEntryComponent) inside our container with createComponent
of a viewRef 🤩
Nice to know: The old way of manually loading a module
Before Angular 6 there was no easy way of creating modules you could easily load. You had to hook up into `ANALYZE_FOR_ENTRY_COMPONENTS` which is uses by the router internally. You would provide an array of fake routes (you wouldn't actually use) to trigger the lazy module generation. {provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
Branch router/guards
You can guard routes — like protecting your admin area from loading without being authenticated or ensure data being loaded before activating a route.
You have the following guard interfaces you can implement.
CanActivate
to mediate navigation to a route.CanActivateChild
to mediate navigation to a child route.CanDeactivate
to mediate navigation away from the current route.Resolve
to perform route data retrieval before route activation.CanLoad
to mediate navigation to a feature module loaded asynchronously.
You can either return a static value or an observable (that must complete). Example of a guard — you can put one at any route and implement multiple guards in one class.
ng g guard my-protector
You get the following file generated code.
@Injectable({
providedIn: 'root'
})
export class MyProtectorGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return true;
}
}
It's like a service and injected as such (providedIn: 'root'
)
Import it and assign it to a guard (here canActivate
).
const routes: Route[] = [
{
path: 'some-route', component: AComponent,
canActivate: [MyProtectorGuard]
}
];
If you navigate to some-route
everything is fine. Change the guard to return false and a route change will be prevented and silently ignored. Actually not silent, you have all possibilities at hand to make some noise in your guard (like redirecting to trigger a message).
Branch router/resolver
Resolvers are close relatives to guards. They are also involved in routing and ensure that data required for a route is guaranteed to be available when a route is activated. We extend the protection guard to be a resolver too and it to the route.
{
path: 'some-route', component: AComponent,
canActivate: [MyProtectorGuard],
resolve: [MyProtectorGuard]
}
By specifying resolve: [MyProtectorGuard]
you register the existing Guard. That Guard needs to implement the interface Resolve<T>
correctly. T is the type of the value your resolver returns. Let's implement the require resolve method in the guard. This time we return an observable instead of a static value.
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any | Observable<any> | Promise<any> {
return of({
hello: 'hello',
items: [1, 2, 3, 6]
}).pipe(delay(1000));
}
You can get hold of the resolved data inside your routed component:
constructor(private route: ActivatedRoute) { }
ngOnInit() {
console.log('ngOnInit', this.route.snapshot.data[0]);
}
ngOnInit {hello: "hello", items: Array(4)}
If you now navigate to the url you will notice a delay of 1 second and then the data being loaded (for a simulated duration of 1s) is resolved and the component is loaded. To display an application wide loading spinner you would implement a spinner component in the application template and control it through a service inside your guards by turning it on or off.
A service that notifies about resolving data being could look like this:
export class RouterLoadingService {
routerLoading: BehaviorSubject<boolean> = new BehaviorSubject(false);
loading$ = this.routerLoading.asObservable();
constructor (private router: Router) {
this.router.events.subscribe(event => {
console.log(event);
if (event instanceof ResolveStart) {
this.routerLoading.next(true);
} else if (event instanceof ResolveEnd) {
this.routerLoading.next(false);
}
});
}
}
That's it 💪 We digged through those important bits of routing:
- Router 1: Router Outlet
- Router 2: Lazy Load
- Router 3: Manual Loading a Module
- Router 4: Guards
- Router 5: Resolver
Continue with Chapter 03 - Routing (Challenge)
router/outlet
router/lazy-load
router/manual-lazy-load
router/guards
router/resolver