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'
}
];
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);
}
}
}
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);
}
}
TitleStrategy
class has an abstract method that must be implemented.
const customTitle = this.buildTitle(snapshot) || '';
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));
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();
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}`)
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);
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
}
]
});
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:
- 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