@isa/common/title-management
Reusable title management patterns for Angular applications with reactive updates and tab integration.
Overview
This library provides two complementary approaches for managing page titles in the ISA application:
IsaTitleStrategy- A custom TitleStrategy for route-based static titlesusePageTitle()- A reactive helper function for component-based dynamic titles
Both approaches automatically:
- Add the ISA prefix from config to all titles
- Update the TabService for multi-tab navigation
- Set the browser document title
When to Use What
| Scenario | Recommended Approach |
|---|---|
| Static page title (never changes) | Route configuration with IsaTitleStrategy |
| Dynamic title based on user input (search, filters) | usePageTitle() in component |
| Title depends on loaded data (item name, ID) | usePageTitle() in component |
| Wizard/multi-step flows with changing steps | usePageTitle() in component |
| Combination of static base + dynamic suffix | Both (route + usePageTitle()) |
Installation
This library is already installed and configured in your workspace. Import from:
import { IsaTitleStrategy, usePageTitle } from '@isa/common/title-management';
Setup
1. Configure IsaTitleStrategy in AppModule
To enable automatic title management for all routes, add the IsaTitleStrategy to your app providers:
// apps/isa-app/src/app/app.module.ts
import { TitleStrategy } from '@angular/router';
import { IsaTitleStrategy } from '@isa/common/title-management';
@NgModule({
providers: [
{ provide: TitleStrategy, useClass: IsaTitleStrategy }
]
})
export class AppModule {}
Note: This replaces Angular's default TitleStrategy with our custom implementation that adds the ISA prefix and updates tabs.
Usage
Static Titles (Route Configuration)
For pages with fixed titles, simply add a title property to your route:
// In your routing module
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
title: 'Dashboard' // Will become "ISA - Dashboard"
},
{
path: 'artikelsuche',
component: ArticleSearchComponent,
title: 'Artikelsuche' // Will become "ISA - Artikelsuche"
},
{
path: 'returns',
component: ReturnsComponent,
title: 'Rückgaben' // Will become "ISA - Rückgaben"
}
];
The IsaTitleStrategy will automatically:
- Add the configured prefix (default: "ISA")
- Update the active tab name
- Set the document title
Dynamic Titles (Component with Signals)
For pages where the title depends on component state, use the usePageTitle() helper:
Example 1: Search Page with Query Term
import { Component, signal, computed } from '@angular/core';
import { usePageTitle } from '@isa/common/title-management';
@Component({
selector: 'app-article-search',
standalone: true,
template: `
<input [(ngModel)]="searchTerm" placeholder="Search..." />
<h1>{{ pageTitle().title }}</h1>
`
})
export class ArticleSearchComponent {
searchTerm = signal('');
// Computed signal that updates when searchTerm changes
pageTitle = computed(() => {
const term = this.searchTerm();
return {
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
};
});
constructor() {
// Title updates automatically when searchTerm changes!
usePageTitle(this.pageTitle);
}
}
Result:
- Initial load:
ISA - Artikelsuche - After searching "Laptop":
ISA - Artikelsuche - "Laptop" - Tab name also updates automatically
Example 2: Detail Page with Item Name
import { Component, signal, computed, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { usePageTitle } from '@isa/common/title-management';
@Component({
selector: 'app-product-details',
standalone: true,
template: `<h1>{{ productName() || 'Loading...' }}</h1>`
})
export class ProductDetailsComponent implements OnInit {
private route = inject(ActivatedRoute);
productName = signal<string | null>(null);
pageTitle = computed(() => {
const name = this.productName();
return {
title: name ? `Produkt - ${name}` : 'Produkt Details'
};
});
constructor() {
usePageTitle(this.pageTitle);
}
ngOnInit() {
// Load product data...
this.productName.set('Samsung Galaxy S24');
}
}
Example 3: Combining Route Title with Dynamic Content
import { Component, signal, computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { usePageTitle } from '@isa/common/title-management';
@Component({
selector: 'app-order-wizard',
standalone: true,
template: `<div>Step {{ currentStep() }} of 3</div>`
})
export class OrderWizardComponent {
private route = inject(ActivatedRoute);
currentStep = signal(1);
pageTitle = computed(() => {
// Get base title from route config
const baseTitle = this.route.snapshot.title || 'Bestellung';
const step = this.currentStep();
return {
title: `${baseTitle} - Schritt ${step}/3`
};
});
constructor() {
usePageTitle(this.pageTitle);
}
}
Nested Components (Parent/Child Routes)
When using usePageTitle() in nested component hierarchies (parent/child routes), the deepest component automatically wins. When the child component is destroyed (e.g., navigating away), the parent's title is automatically restored.
This happens automatically - no configuration or depth tracking needed!
Example: Dashboard → Settings Flow
// Parent route: /dashboard
@Component({
selector: 'app-dashboard',
standalone: true,
template: `<router-outlet />`
})
export class DashboardComponent {
pageTitle = signal({ title: 'Dashboard' });
constructor() {
usePageTitle(this.pageTitle);
// Sets: "ISA - Dashboard"
}
}
// Child route: /dashboard/settings
@Component({
selector: 'app-settings',
standalone: true,
template: `<h1>Settings</h1>`
})
export class SettingsComponent {
pageTitle = signal({ title: 'Settings' });
constructor() {
usePageTitle(this.pageTitle);
// Sets: "ISA - Settings" (child wins!)
}
}
// Navigation flow:
// 1. Navigate to /dashboard → Title: "ISA - Dashboard"
// 2. Navigate to /dashboard/settings → Title: "ISA - Settings" (child takes over)
// 3. Navigate back to /dashboard → Title: "ISA - Dashboard" (parent restored automatically)
How It Works
The library uses an internal registry that tracks component creation order:
- Last-registered (deepest) component controls the title
- Parent components' title updates are ignored while child is active
- Automatic cleanup via Angular's
DestroyRef- when child is destroyed, parent becomes active again
Real-World Scenario
// Main page with search
@Component({...})
export class ArticleSearchComponent {
searchTerm = signal('');
pageTitle = computed(() => {
const term = this.searchTerm();
return {
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
};
});
constructor() {
usePageTitle(this.pageTitle); // "ISA - Artikelsuche"
}
}
// Detail view (child route)
@Component({...})
export class ArticleDetailsComponent {
articleName = signal('Samsung Galaxy S24');
pageTitle = computed(() => ({
title: `Artikel - ${this.articleName()}`
}));
constructor() {
usePageTitle(this.pageTitle); // "ISA - Artikel - Samsung Galaxy S24" (wins!)
}
}
// When user closes detail view → "ISA - Artikelsuche" is restored automatically
Important Notes
✅ Works with any nesting depth - Grandparent → Parent → Child → Grandchild
✅ No manual depth tracking - Registration order determines precedence
✅ Automatic restoration - Parent title restored when child is destroyed
⚠️ Parent signal updates are ignored while child is active (by design!)
Tab Subtitles
You can include a subtitle in the signal to display additional context in the tab:
constructor() {
this.pageTitle = signal({
title: 'Dashboard',
subtitle: 'Active Orders'
});
usePageTitle(this.pageTitle);
}
Use Cases for Subtitles:
- Status indicators:
"Pending","Active","Completed" - Context information:
"3 items","Last updated: 2min ago" - Category labels:
"Customer","Order","Product" - Step indicators:
"Step 2 of 5","Review"
Example: Order Processing with Status
@Component({
selector: 'app-order-details',
standalone: true,
template: `
<h1>Order {{ orderId() }}</h1>
<p>Status: {{ orderStatus() }}</p>
`
})
export class OrderDetailsComponent {
orderId = signal('12345');
orderStatus = signal<'pending' | 'processing' | 'complete'>('pending');
// Status labels for subtitle
statusLabels = {
pending: 'Awaiting Payment',
processing: 'In Progress',
complete: 'Completed'
};
pageTitle = computed(() => ({
title: `Order ${this.orderId()}`,
subtitle: this.statusLabels[this.orderStatus()]
}));
constructor() {
// Title and subtitle both update dynamically
usePageTitle(this.pageTitle);
}
}
Example: Search Results with Count
@Component({
selector: 'app-article-search',
standalone: true,
template: `...`
})
export class ArticleSearchComponent {
searchTerm = signal('');
resultCount = signal(0);
pageTitle = computed(() => {
const term = this.searchTerm();
const count = this.resultCount();
return {
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche',
subtitle: `${count} Ergebnis${count === 1 ? '' : 'se'}`
};
});
constructor() {
usePageTitle(this.pageTitle);
}
}
Optional Title and Subtitle
Both title and subtitle are optional. When a property is undefined, it will not be updated:
Skip Title Update (Subtitle Only)
// Only update the subtitle, keep existing document title
pageTitle = signal({ subtitle: '3 items' });
usePageTitle(this.pageTitle);
Skip Subtitle Update (Title Only)
// Only update the title, no subtitle
pageTitle = signal({ title: 'Dashboard' });
usePageTitle(this.pageTitle);
Skip All Updates
// Empty object - skips all updates
pageTitle = signal({});
usePageTitle(this.pageTitle);
Conditional Updates
// Skip title update when data is not loaded
pageTitle = computed(() => {
const name = this.productName();
return {
title: name ? `Product - ${name}` : undefined,
subtitle: 'Loading...'
};
});
usePageTitle(this.pageTitle);
Migration Guide
From Resolver-Based Titles
If you're currently using a resolver for titles (e.g., resolveTitle), here's how to migrate:
Before (with resolver):
// In resolver file
export const resolveTitle: (keyOrTitle: string) => ResolveFn<string> =
(keyOrTitle) => (route, state) => {
const config = inject(Config);
const title = inject(Title);
const tabService = inject(TabService);
const titleFromConfig = config.get(`process.titles.${keyOrTitle}`, z.string().default(keyOrTitle));
// ... manual title setting logic
return titleFromConfig;
};
// In routing module
{
path: 'dashboard',
component: DashboardComponent,
resolve: { title: resolveTitle('Dashboard') }
}
After (with IsaTitleStrategy):
// No resolver needed - just use route config
{
path: 'dashboard',
component: DashboardComponent,
title: 'Dashboard' // Much simpler!
}
For dynamic titles, use usePageTitle() instead:
// In component
pageTitle = computed(() => ({ title: this.dynamicTitle() }));
constructor() {
usePageTitle(this.pageTitle);
}
API Reference
IsaTitleStrategy
Custom TitleStrategy implementation that extends Angular's TitleStrategy.
Methods:
updateTitle(snapshot: RouterStateSnapshot): void- Called automatically by Angular Router
Dependencies:
@isa/core/config- For title prefix configuration@isa/core/tabs- For tab name updates@angular/platform-browser- For document title updates
Configuration:
// In app providers
{ provide: TitleStrategy, useClass: IsaTitleStrategy }
usePageTitle(titleSubtitleSignal)
Reactive helper function for managing dynamic component titles and subtitles.
Parameters:
titleSubtitleSignal: Signal<PageTitleInput>- A signal containing optional title and subtitle
Returns: void
Dependencies:
@isa/core/config- For title prefix configuration@isa/core/tabs- For tab name updates@angular/platform-browser- For document title updates
Example:
const pageTitle = computed(() => ({
title: `Search - ${query()}`,
subtitle: `${count()} results`
}));
usePageTitle(pageTitle);
PageTitleInput
Input interface for usePageTitle().
Properties:
title?: string- Optional page title (without ISA prefix). When undefined, document title is not updated.subtitle?: string- Optional subtitle to display in the tab. When undefined, tab subtitle is not updated.
Best Practices
✅ Do
- Use route-based titles for static pages
- Use
usePageTitle()for dynamic content-dependent titles - Keep title signals computed from other signals for reactivity
- Use descriptive, user-friendly titles
- Combine route titles with component-level refinements for clarity
- Use subtitles for status indicators, context info, or step numbers
- Return
undefinedfor title/subtitle when you want to skip updates
❌ Don't
- Add the "ISA" prefix manually (it's added automatically)
- Call
Title.setTitle()directly (use these utilities instead) - Create multiple effects updating the same title (use one computed signal)
- Put long, complex logic in title computations (keep them simple)
Examples from ISA Codebase
Artikelsuche (Search with Term)
@Component({...})
export class ArticleSearchComponent {
searchTerm = signal('');
pageTitle = computed(() => {
const term = this.searchTerm();
return {
title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
};
});
constructor() {
usePageTitle(this.pageTitle);
}
}
Rückgabe Details (Return with ID)
@Component({...})
export class ReturnDetailsComponent {
returnId = signal<string | null>(null);
pageTitle = computed(() => {
const id = this.returnId();
return {
title: id ? `Rückgabe - ${id}` : 'Rückgabe Details'
};
});
constructor() {
usePageTitle(this.pageTitle);
}
}
Testing
Run tests for this library:
npx nx test common-title-management --skip-nx-cache
Architecture Notes
This library is placed in the common domain (not core) because:
- It's a reusable utility pattern that features opt into
- Components can function without it (unlike core infrastructure)
- Provides patterns for solving a recurring problem (page titles)
- Similar to other common libraries like decorators and data-access utilities
Related Libraries
@isa/core/config- Configuration management@isa/core/tabs- Multi-tab navigation@isa/core/navigation- Navigation context preservation
Support
For issues or questions, refer to the main ISA documentation or contact the development team.