Update page title with Title Strategy in Angular

Connie Leung - Jan 1 - - Dev Community

Introduction

In this blog post, I described how to update page title using custom title strategy class. Since Angular 14, a route has an optional title property that sets document title after navigation. In some use cases, a generic document title is suffice. In other use cases, pages display dynamic contents and the document title needs to display dynamic texts. For example, the product details page wishes to display product name in the document title instead of a generic text such as "Product".

When I had to update page title programmatically, I added a custom title strategy class that extended TitleStrategy. The custom title strategy class provided logic in the updateTitle method to derive page title based on path parameters.

Use case of the demo

When the application routes to /product-list or /my-cart, both pages display a generic title. When the application routes to /products/:id or /categories/:category, the page title should be descriptive and derived from path parameter (id or category).

Application routes

// routes.ts

import { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: 'products',
    loadComponent: () => import('./categories/product-catalogue/product-catalogue.component').then((m) => m.ProductCatalogueComponent),
    title: 'Product list',
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
    title: 'Product',
  },
    {
    path: 'my-cart',
    loadComponent: () => import('./carts/cart/cart.component').then((m) => m.CartComponent),
    title: 'My shopping cart',
  },
  {
    path: 'categories/:category',
    loadComponent: () => import('./categories/category-products/category-products.component').then((m) => m.CategoryProductsComponent),
    title: 'Category',
  },
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'products',
  },
  {
    path: '**',
    redirectTo: 'products'
  }
];
Enter fullscreen mode Exit fullscreen mode

In the demo, there is a route.ts file that defines all the routes to different paths. I would like the the title of products page and my-cart page be static. When the application navigates to /products, the title of the tab is "Product List". Similarly, the document tab displays "My shopping cart" when the application routes to /my-cart. On the other hand, the product details and category pages display "Product - " and "Category - " that are dynamic. Therefore, I implemented a custom title strategy class that built page title based on path parameter.

Update page title with custom title strategy class

// shop-page-title.strategy.ts

import { inject, Injectable } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
import { map, Subscription } from 'rxjs';
import { ProductService } from './products/services/product.service';

@Injectable()
export class ShopPageTitleStrategy extends TitleStrategy {
  title = inject(Title);
  productService = inject(ProductService);
  subscription: Subscription | null = null;

  updateTitle(snapshot: RouterStateSnapshot): void {
    this.subscription?.unsubscribe();

    const customTitle = this.buildTitle(snapshot) || '';
    const firstChild = snapshot.root.firstChild;
    const productId = firstChild?.params['id'] || '';
    const category = firstChild?.params['category'] || '';
    if (productId) {
      this.subscription = this.productService.getProduct(productId)
        .pipe(
          map((product) => product?.title || ''),
          map((productTitle) => `${customTitle} - ${productTitle}`),
        )
        .subscribe((pageTitle) => this.title.setTitle(pageTitle));
    } else if (category) {
      this.title.setTitle(`${customTitle} - ${category}`)
    } else {
      this.title.setTitle(customTitle);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This custom strategy class extends TitleStrategy class and handles 3 cases

  • Display the title as-is when the current path has neither id nor category parameter
  • Display "Product - " when path parameter, id, exists
  • Display "Category - " when path parameter, category, exists
updateTitle(snapshot: RouterStateSnapshot): void {
    this.subscription?.unsubscribe();

    const customTitle = this.buildTitle(snapshot) || '';
    const firstChild = snapshot.root.firstChild;
    const productId = firstChild?.params['id'] || '';
    const category = firstChild?.params['category'] || '';
    if (productId) {
      this.subscription = this.productService.getProduct(productId)
        .pipe(
          map((product) => product?.title || ''),
          map((productTitle) => `${customTitle} - ${productTitle}`),
        )
        .subscribe((pageTitle) => this.title.setTitle(pageTitle));
    } else if (category) {
      this.title.setTitle(`${customTitle} - ${category}`)
    } else {
      this.title.setTitle(customTitle);
    }
  }
Enter fullscreen mode Exit fullscreen mode

TitleStrategy class has an abstract method that must be implemented.

const customTitle = this.buildTitle(snapshot) || '';
Enter fullscreen mode Exit fullscreen mode

this.buildTitle(snapshot) returns the title of the route (Product List, Category, Product or Category).

this.subscription = this.productService.getProduct(productId)
        .pipe(
          map((product) => product?.title || ''),
          map((productTitle) => `${customTitle} - ${productTitle}`),
        )
        .subscribe((pageTitle) => this.title.setTitle(pageTitle));
Enter fullscreen mode Exit fullscreen mode

When the path has id parameter, I invoke ProductService to retrieve a product Observable by the product id. Then, I pipe map operator twice to build the dynamic product title. I don't know when ShopPageTitleStrategy completes the Observable; therefore, I subscribe the Observable and assign the subscription to the subscription member. Moreover, subscribe invokes Title service to set the document title.

this.subscription?.unsubscribe();
Enter fullscreen mode Exit fullscreen mode

The above line of code unsubscribes the previous subscription before updating page title. The purpose is to prevent memory leak.

this.title.setTitle(`${customTitle} - ${category}`)
Enter fullscreen mode Exit fullscreen mode

When the path has category parameter, I concatenate the category to the custom title and set the document title to the value.

this.title.setTitle(customTitle);
Enter fullscreen mode Exit fullscreen mode

When the path does not have any path parameter, the document title is updated to the static route title.

Register Custom Title Strategy class

// main.ts

import { ShopPageTitleStrategy } from './shop-page-title.strategy';

bootstrapApplication(App, {
  providers: [
    ... other providers ...,
    {
      provide: TitleStrategy,
      useClass: ShopPageTitleStrategy
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

In bootstrapApplication function, ShopPageTitleStrategy class is registered to provide the implementation of TitleStrategy class.

We are done here. When the application navigates to different path, the document title is set according to the logic in ShopPageTitleStrategy class.

The following Stackblitz repo shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .