Update page title with Title Strategy in Angular
Tags: tutorial, angular, webdev, typescript
Published: 2024-01-01 13:14:06

##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

```typescript
// 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'
  }
];
```

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 - <product name>" and "Category - <category name>" 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

```typescript
// 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);
    }
  }
}
```

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 - <product name>" when path parameter, id, exists
- Display "Category - <category name>" when path parameter, category, exists

```typescript
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);
    }
  }
```

`TitleStrategy` class has an abstract method that must be implemented.  

```typescript
const customTitle = this.buildTitle(snapshot) || '';
```

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

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

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.    

```typescript
this.subscription?.unsubscribe();
```

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

```typescript
this.title.setTitle(`${customTitle} - ${category}`)
```

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

```typescript
this.title.setTitle(customTitle);
```

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

###Register Custom Title Strategy class

```typescript
// main.ts

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

bootstrapApplication(App, {
  providers: [
    ... other providers ...,
    {
      provide: TitleStrategy,
      useClass: ShopPageTitleStrategy
    }
  ]
});
```

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:
{% embed https://stackblitz.com/edit/stackblitz-starters-t249xj?file=src%2Fmain.ts %}

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:

- Github Repo: https://github.com/railsstudent/ng-online-store-demo-ngxtension/blob/main/src/app/shop-page-title.strategy.ts
- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-t249xj?file=src%2Fshop-page-title.strategy.ts
- Angular documentation:  https://angular-dev-site.web.app/guide/routing/common-router-tasks#setting-the-page-title