Search code examples
angularangular-routingangular-router

Angular setting title depending on activated route and its parents


My goal: I would like to globally listen for router NavigationEnd events and change the title of the web page depending on the route custom data.

Current code (in app.component.ts):

this.router.events.pipe(
  filter((ev) => ev instanceof NavigationEnd),
  map(() => this.activatedRoute),
  map((route) => {
    while (route.firstChild) route = route.firstChild;
    return route;
  }),
  filter((route) => route.outlet === 'primary'),
  mergeMap((route) => route.data),
).subscribe((event) => this.title.setTitle(event['title']));

got from this tutorial

The above code works, but is not elegant at all, look at the needed routing configuration:

app-routing.module.ts

const routes: Routes = [
  {
    path: 'user',
    loadChildren: () => import(`./user/user.module`).then(m => m.UserModule),
    data: { title: "Pagina Utente" }
  },
  {
    path: 'admin', loadChildren: () => import(`./admin/admin.module`).then(m => m.AdminModule),
    data: { title: "Pannello Admin" }
  },

  { path: '**', component: HomeComponent },
];

admin-routing.module.ts:

const routes: Routes = [
  {
    path: '', component: AdminPanelComponent,
    children: [
      {
        path: 'users', component: UsersComponent,
        data: { title: 'Pannello Admin: Utenti' }
      },
      {
        path: 'comuni', component: ComuniComponent,
        data: { title: 'Pannello Admin: Comuni' }
      },

    ]
  }
];

As you can see, i already specified that the /admin route should have "Pannello Admin" as a title, but i need to write it again and again for each of its children routes.

I tried to fix this problem by changing the app.component.ts code to this:

this.router.events.pipe(
  filter((ev) => ev instanceof NavigationEnd),
  map(() => this.activatedRoute),
  map((route) => {
    let title = route.data['title'] || '';
    while (route.firstChild) {
      route = route.firstChild;
      title+= route.data['title'];
    }
    return route;
  }),
  filter((route) => route.outlet === 'primary'),
  mergeMap((route) => route.data),
).subscribe((event) => this.title.setTitle(event['title']));

I expected this code to work and set my title to the concatenation of the data['title'] property of the matched routes. However it doesn't work and just gives out empty titles (the data['title'] are always undefined for some reason)


Solution

  • Since Angular 14, this can be handled natively.

    So, you can write the routes like below.

    app-routing.module.ts

    const routes: Routes = [
      {
        path: 'user',
        loadChildren: () => import(`./user/user.module`).then(m => m.UserModule),
        title: "Pagina Utente" // <--
      },
      {
        path: 'admin', loadChildren: () => import(`./admin/admin.module`).then(m => m.AdminModule),
        title: "Pannello Admin" // <--
      },
    
      { path: '**', component: HomeComponent },
    ];
    

    admin-routing.module.ts:

    const routes: Routes = [
      {
        path: '', component: AdminPanelComponent,
        children: [
          {
            path: 'users', component: UsersComponent,
            title: 'Utenti' // <--
          },
          {
            path: 'comuni', component: ComuniComponent,
            title: 'Comuni' // <--
          },
    
        ]
      }
    ];
    

    The default TitleStrategy will result in showing the title of the end node of the routing tree. So, you'll see only Utenti when you visit /admin/users for example.

    You can set 'Pannello Admin: Utenti' to the title property instead of 'Utenti', but overriding TitleStrategy should be more effective.

    You can see a simple sample code to override the strategy in Overriding The Global Title Strategy section of the article here

    To build titles like Pannello Admin: Utenti from the routes above, the custom TitleStrategy would be like:

    @Injectable({providedIn: 'root'})
    export class ConcatTitleStrategy extends TitleStrategy {
      constructor(
        private readonly title: Title
      ) {
        super();
      }
    
      override updateTitle(routerState: RouterStateSnapshot): void {
        const title = this.concatTitle(routerState.root, '', ': ');
        if (title) {
          this.title.setTitle(title);
        }
      }
    
      private concatTitle(route: ActivatedRouteSnapshot, title: string, separator: string): string {
        if (!route)
          return title;
    
        const sub = route.data ? this.getResolvedTitleForRoute(route) : undefined;
        if (sub) {
          title = `${title}${separator}${sub}`;
        }
    
        title = this.concatTitles(route.children[0], title, separator);
    
        return title;
      }
    }