1
\$\begingroup\$

In an angular application I would like to set the browser title to something that is helpful. (Helpful means that the user gets a meaningful history and can identify tabs easily). Setting the title should be as automatic as possible. That means:

  • If the route changes and the component that becomes active has title, then it should be used
  • If the route changes and the component that becomes active has no title a default title should be used
  • Dynamic titles should be allowed, so e.g. in the hero-app I might want to display the name of the hero currently displayed in the title
  • The title must allow localization

Angular does have a the Title-Service to set the title, but it is setter-based, so I'd have to make sure that each and every route/component sets a title or every component that sets a tile needs to unset it in ngOnDestroy. That is already quite cumbersome and then there are situations where this won't work at all, because multiple components might override each other.

I could not find an existing solution, so I created my own. It looks like this:

  1. I create my own TitleService which wraps angular's service:

    import { Injectable } from '@angular/core';
    import { debounceTime, switchMap } from 'rxjs/operators';
    import { Title } from '@angular/platform-browser';
    import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
    
    
    @Injectable({
        providedIn: 'root'
    })
    export class TitleService {
        private titleSegments = new BehaviorSubject<Observable<string>[]>([of('My App')]);
    
        constructor(private delegate: Title) {
            this.titleSegments
                .pipe(
                    switchMap((values: Observable<string>[]) => combineLatest(values)),
                    debounceTime(100) // Prevents "flickering", if during loading the title changes a lot
                )
                .subscribe(segments => this.delegate.setTitle(segments.join(' < ')));
        }
    
        addTitleSegment(segment: Observable<string>) {
            this.titleSegments.next([segment, ...this.titleSegments.value]);
        }
    
        popTitleSegment() {
            this.titleSegments.next(this.titleSegments.value.slice(1));
        }
    }
    
    
  2. I create a directive and a component to interact with mit TitleService. Directive:

    import { Directive, ElementRef, Input, OnDestroy } from '@angular/core';
    import { ReplaySubject } from 'rxjs';
    import { TitleService } from '../core/title.service';
    
    @Directive({
        selector: '[appPageTitle]'
    })
    export class PageTitleDirective implements OnDestroy {
        private title$ = new ReplaySubject<string>(1);
    
        constructor(private titleService: TitleService, el: ElementRef) {
            this.title$.subscribe(title => el.nativeElement.innerText = title);
            titleService.addTitleSegment(this.title$);
        }
    
        @Input()
        set appPageTitle(title: string) {
            this.title$.next(title);
        }
    
        ngOnDestroy(): void {
            this.titleService.popTitleSegment();
        }
    }
    

    Component:

        import { Component, Input, OnDestroy } from '@angular/core';
    import { TitleService } from '../core/title.service';
    import { ReplaySubject } from 'rxjs';
    
    @Component({
        selector: 'app-page-title',
        template: '',
    })
    export class PageTitleComponent implements OnDestroy {
        private title$ = new ReplaySubject<string>(1);
    
        constructor(private titleService: TitleService) {
            titleService.addTitleSegment(this.title$);
        }
    
        @Input()
        set title(title: string) {
            this.title$.next(title);
        }
    
        ngOnDestroy(): void {
            this.titleService.popTitleSegment();
        }
    }
    

    The component and the directive are very similar. I use the directive most of the time, so I can use heading and title in one go, but in case there is no heading I use the directive, which has no effect on the UI.

Now I can simply use the directive/component in my HTML. E.g. like this:

root.html:

<h1 appPageTitle="Heroes"></h1>
<app-edit-hero></app-edit-hero>

edit.html:

<h1 [appPageTitle]="'edit' + hero.name"></h1>
...

And in this case I get (if hero.name === 'My Hero'):

edit My Hero < Heroes < MY APP

As browser title, which looks great. Also it is very easy to manage, since this happens mostly automatically, whenever routes change or the data I would like changes.

Questions:

  • Are there any hidden pitfalls? E.g. I depend on the correct order of construction and destruction of components when navigating. Are there any cases where this might be a problem?
  • Is this OK with standard angular coding best practices?
  • Is there anything from the usability/accessibility perspective that might be a problem?
  • Any general suggestions of improvement?
  • Are there other existing approaches, that I have missed during my research?
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.