Angular Named RouterOutlets

Angular Named RouterOutlets
Photo by Kyle Gregory Devaras / Unsplash

The Angular Router is an essential part of most Angular applications.  It has a lot of capabilities but often much of it is not used.  In this post we'll look at named routes and how they can be used.  Named routes are also sometimes referred to as secondary or auxillary routes.  For clarity we'll refer to them as secondary named routes.

Consider the app below, it allows users to browse recipes and update a shopping list as they go through each recipes list of ingredients.  The shopping list should always be visible if the user wants to see what they have already added to their shopping list.  So the app will load the ShoppingListComponent as a secondary named route.  

0:00
/
Recipe Ace app with shopping list

You can find the sample app here.

Breakdown

Lets start by breaking down the pieces needed to add a secondary named route to an application.

When you create a new Angular application and choose to enable routing a <router-outlet> directive will be added to your app.component.html template file.  This is known as the primary route.  It can be placed in any template file.

<router-outlet></router-outlet> is the primary route of an application.

<router-outlet name="shopping"></router-outlet> is an example of a secondary named route.  A secondary named route can load additional content regardless of primary route.  So it is a great option for content that needs to be independent of the primary route (think sticky), e.g. help content, documentation, popups, shopping cart, etc.  

Each secondary named route will lazy load a component:

{ path: "list", component: ShoppingListComponent, outlet: "shopping" }

You can link to a named outlet in you template:

[routerLink]="[{ outlets: { shopping: ['list'] } }]"

This will produce a URL like http://localhost:4200/recipes(shopping:list)

Note that above there is no primary path in our template link, just the secondary named router outlet "shopping" pointing to the "list" path.

The (shopping:list) part is how you can tell it is a secondary named route.  This URL can be modified if you don't like the look of the () by using Angular's https://angular.io/api/router/UrlSerializer  (more on this below).

The ShoppingListComponent will be loaded as a secondary named route, meaning users can choose whether they want to see it on every recipe or not.  When they route to a new recipe the ShoppingListComponent will be shown if they chose to open it before routing to the current recipe.  You can think of it as sticky functionality.

The routes need to be defined, notice the outlet property below.

const routes: Routes = [
	{ path: "", pathMatch: "full", redirectTo: 'recipes' },
	{ path: "recipes", component: RecipeContainerComponent },
	{ path: "recipe/:name", component: RecipeComponent },
	{ path: "list", component: ShoppingListComponent, outlet: "shopping" }
];

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

The same functionality could be achieved without named routes except for the fact that the link to a particular recipe will be different.  With a secondary named route sharing a link will take the user to exactly the same route, which is not necessarily true for a pure component approach as it may be dependent on state.

Customizing URL

Angular provides a DefaultUrlSerializer which can be extended if you want to customize a URL.  There are two methods that you can override in the class that extends the DefaultUrlSerializer.

  • parse() parses the URL and converts it into an UrlTree
  • serialize() takes an UrlTree and converts it into an URL
DefaultUrlSerializer uses parentheses to serialize secondary segments (e.g., popup:compose), the colon syntax to specify the outlet, and the ';parameter=value' syntax (e.g., open=true) to specify route specific parameters.

Below shows a custom implementation that overrides the serialize method of DefaultUrlSerializer.

export class CustomUrlSerializer extends DefaultUrlSerializer {

  /**
   * Replace () with / 
   * e.g. recipes(shopping:list) -> recipes/shopping:list
   * @param url 
   * @returns 
   */
  private customizeUrl(url: string): string {
    return url
      .replace('(', '/')
      .replace(')', '')
      .split('//').join('/');
  }

  override serialize(tree: UrlTree): string {
    return this.customizeUrl(super.serialize(tree));
  }
}

Then you must provide this class to make Angular aware it needs to use your implementation.

providers: [
  { provide: UrlSerializer, useClass: CustomUrlSerializer }
]

This will transform our URL from http://localhost/recipes(shopping:list) to http://localhost/recipes/shopping:list.

Clearing the URL

When the user clicks the Delete shopping list button we need to do two key things.

  1. Clear the secondary named route from the URL bar.
  2. Delete the contents of the shopping list.

We can clear the URL bar by using the following method:

deleteShoppingList() {
  // remove named route from URL
  this.router.navigate(['.', { outlets: { shopping: null } }], {
    relativeTo: this.route.parent
  });

  // clear shopping list
  this.recipeService.clearShoppingList();
    
  // close side bar
  this.drawer?.close();
}

Wrap Up

Angular named secondary routes can be a helpful addition to your application if you have a need to load components that are independed of the primary route, with the added bonus of the component being lazy loaded.

Key take aways:

  1. You can have many secondary named routes associated with a template.
  2. These components will be lazy loaded.
  3. Secondary named routes are independent, they can be displayed on any primary route.
  4. You can link to to views that will display secondary named routes.