An Overview of Advancements in Angular Router
New Router API for standalone. Angular 14.2 introduced a way to use the Angular Router without the need for RouterModule and improved tree shaking for reduced bundle sizes. First, let’s focus on the new router integration API.
Then’s how to use the router without a RouterModule. In the bootstrapApplication function, we give the router configuration to the provider array using the provideRouter function. The function accepts an array of operation routes. Then’s an illustration
const appRoutes: Routes = […]; bootstrapApplication(AppComponent, { providers: [ provideRouter(appRoutes), ] });
Compare this to the being way to set up the Router in an application that needed a module:
const appRoutes: Routes = […]; bootstrapApplication(AppComponent, { providers: [ importProvidersFrom(RouterModule.forRoot(appRoutes)), ] });
Tree shaking
Developers can completely overhaul the router API by using the new provideRouter API. This is the firts time we’ve had the capability to enable this position of tree-shaking in the Angular router. Now teams can enjoy lower pack sizes.
The design of the RouteModule API prevents bundlers from being suitable to remove unused Router features. With the RouteModule API, the following features are included in the production bundle indeed if not enabled.
HashLocationStrategy — generally used for heritage routing via updating the fragment rather than the path preloading, scrolling, and logic for blocking app lading until navigation homestretches for SSR.
The new provideRouter API changes this geste. The features in the list above are no longer included in the production bundle if they aren’t enabled.
In our testing with the new API, we found that removing these unused features from the bundle redounded in an 11% reduction in the size of the router law in the application bundle when no features are enabled.
Here’s an example of opting into preloading using the withPreloading function :
const appRoutes: Routes = []; bootstrapApplication(AppComponent, { providers: [ provideRouter(appRoutes, withPreloading(PreloadAllModules)) ] });
Now supporting functional router guards
Router guards require too much boilerplate….
-Most Angular devs undoubtedly did so at some point.
We’ve entered feedback from multiple developers that amounts to developers wanting lower boilerplate and further productivity. Let’s cover some of the instigative new changes that move us closer to this thing.
Functional router guards with the fit() are lightweight, ergonomic, and more composable than class- grounded guards.
Then’s an illustration of a functional guard that takes a component as a parameter and returns whether or not a route can be deactivated grounded on the component’s hasUnsavedChanges property:
const route = { path: ‘edit’, component: EditCmp, canDeactivate: [ (component: EditCmp) => !component.hasUnsavedChanges ] };
Additionally, some useful guards continue to support inject from @angular/core to inject dependencies:
const route = { path: ‘admin’, canActivate: [() => inject(LoginService).isLoggedIn()] };
The Router APIs preliminarily needed guards and resolvers to be present in Angular’s reliance injection. This resulted in unnecessary boilerplate law. Let’s consider two examples.
In the first illustration, then’s the law needed to give a guard using an InjectionToken:
const UNSAVED_CHANGES_GUARD = new InjectionToken<any>('my_guard', { providedIn: 'root', factory: () => (component: EditCmp) => !component.hasUnsavedChanges }); const route = { path: 'edit', component: EditCmp, canDeactivate: [UNSAVED_CHANGES_GUARD] };
In this second example, here’s the code required for providing guards as an injectable class:
@Injectable({providedIn: 'root'}) export class LoggedInGuard implements CanActivate { constructor(private loginService: LoginService) {} canActivate() { return this.loginService.isLoggedIn(); } } const route = { path: 'admin', canActivate: [LoggedInGuard] };
Notice that indeed when we want to write a simple guard that has no dependencies as in the first illustration, we still have to write either an InjectionToken or an Injectable class. With functional guards developers can produce guards, indeed guards with dependences, with much lower boilerplate.
Functional guards are composable
Functional guards are also more composable. A common point request for the Router is to provide the option for guards to execute sequentially rather than all at formerly. Without this option in the Router enforcing this type of geste in an application would be delicate.
This is smoother to apply with functional guards. There’s indeed an illustration test in the router law to demonstrate this geste.
Let’s review the law to get a better understanding of how this works:
function runSerially(guards: CanActivateFn[]| CanActivateChildFn[]): CanActivateFn|CanActivateChildFn { return ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const injector = inject(EnvironmentInjector); const observables = guards.map(guard => { const guardResult = injector.runInContext( () => guard(route, state)); return wrapIntoObservable(guardResult).pipe(first()); }); return concat(…observables).pipe( takeWhile(v => v === true), last()); }; }
With functional guards, you can produce plant- suchlike functions that accept a configuration and return a guard or resolver function. We now have customizable guards and resolvers, which is another typical point requirement, thanks to this pattern. The runSerially function is a plant- suchlike function that accepts a list of guard functions and returns a new guard.
function runSerially(guards: CanActivateFn[]|CanActivateChildFn[]):
CanActivateFn|CanActivateChildFn
To start, we use: to establish a reference to the current injector:
const injector = inject(EnvironmentInjector);
When functional guards want to use dependency injection( DI), they must call fit synchronously. Each guard may execute asynchronously and may fit dependencies of their own. We need the reference to the current injector incontinently so we can keep running each of them inside the same injection environment:
const guardResult = injector.runInContext(() => guard(route, state));
This is also exactly how the router enables the function guards feature.
Because guards can return an Observable, a Promise, or a synchronous result, we use our wrapIntoObservable helper to transfigure all results to an Observable and take only the first emitted value:
return wrapIntoObservable(guardResult).pipe(first());
We take all of these streamlined guard functions, execute them one after another( with concat), and only subscribe to the result while each of them indicates the route can be activated (i.e, returning true):
concat(…observables).pipe(takeWhile(v => v === true), last());
That’s it. We enabled running functional guards successionally with lower than 10 lines of code. Now, we can call it as follows in our Route configuration:
{ path: ‘hello-world’, component: HelloWorld, canActivate: [runSerially(guard1, guard2, guard3)] }