All files / app/services page-title.strategy.ts

98.11% Statements 52/53
96.42% Branches 27/28
100% Functions 6/6
100% Lines 49/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 964x 4x 4x 4x   4x 4x 4x     4x 15x 15x 15x 15x     14x   14x 10x 10x 10x 10x 10x   4x 4x     14x                       14x 14x 4x 4x     10x 10x 7x 7x 6x     4x       10x 9x   1x                     10x 10x 11x 11x 7x     3x       14x 14x 14x 14x 14x 2x 2x   14x      
import { inject, Injectable, signal } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRouteSnapshot, RouterStateSnapshot, TitleStrategy } from '@angular/router';
import { LoggerService } from '@drevo-web/core';
 
const DEFAULT_TITLE = 'Древо';
const TITLE_SUFFIX = ' - Древо';
const MAX_TITLE_LENGTH = 50;
 
@Injectable()
export class PageTitleStrategy extends TitleStrategy {
    private readonly title = inject(Title);
    private readonly logger = inject(LoggerService).withContext('PageTitleStrategy');
    private readonly _pageTitle = signal(DEFAULT_TITLE);
    readonly pageTitle = this._pageTitle.asReadonly();
 
    override updateTitle(snapshot: RouterStateSnapshot): void {
        const resolved = this.resolveTitle(snapshot);
 
        if (resolved) {
            this._pageTitle.set(resolved.title);
            const truncated = this.truncateTitle(resolved.title);
            const titlePrefix = resolved.route.data['titlePrefix'] as string | undefined;
            const docTitle = titlePrefix ? `${titlePrefix} ${truncated}` : truncated;
            this.title.setTitle(`${docTitle}${TITLE_SUFFIX}`);
        } else {
            this._pageTitle.set(DEFAULT_TITLE);
            this.title.setTitle(DEFAULT_TITLE);
        }
 
        this.logger.debug('Title updated', { title: resolved?.title ?? DEFAULT_TITLE });
    }
 
    /**
     * Resolve page title and the route it came from.
     * First tries standard `buildTitle()` (explicit `title` on the route).
     * If no explicit title, searches route chain for `titleSource` data key
     * and reads title from the resolved data object (e.g. `data['article'].title`).
     */
    private resolveTitle(
        snapshot: RouterStateSnapshot,
    ): { readonly title: string; readonly route: ActivatedRouteSnapshot } | undefined {
        const builtTitle = this.buildTitle(snapshot);
        if (builtTitle) {
            const chain = this.getRouteChain(snapshot);
            return { title: builtTitle, route: chain[chain.length - 1] };
        }
 
        const result = this.findRouteData(snapshot, 'titleSource');
        if (result) {
            const resolved = result.route.data[result.value as string] as { readonly title: string } | undefined;
            if (resolved?.title) {
                return { title: resolved.title, route: result.route };
            }
        }
        return undefined;
    }
 
    private truncateTitle(title: string): string {
        if (title.length <= MAX_TITLE_LENGTH) {
            return title;
        }
        return title.slice(0, MAX_TITLE_LENGTH) + '…';
    }
 
    /**
     * Search for a data key from leaf route up to root.
     * Returns the value and the route where it was found.
     */
    private findRouteData(
        snapshot: RouterStateSnapshot,
        key: string,
    ): { readonly value: unknown; readonly route: ActivatedRouteSnapshot } | undefined {
        const chain = this.getRouteChain(snapshot);
        for (let i = chain.length - 1; i >= 0; i--) {
            const value = chain[i].data[key];
            if (value !== undefined) {
                return { value, route: chain[i] };
            }
        }
        return undefined;
    }
 
    private getRouteChain(snapshot: RouterStateSnapshot): ActivatedRouteSnapshot[] {
        const chain: ActivatedRouteSnapshot[] = [];
        Iif (!snapshot.root) return chain;
        let route: ActivatedRouteSnapshot = snapshot.root;
        chain.push(route);
        while (route.firstChild) {
            route = route.firstChild;
            chain.push(route);
        }
        return chain;
    }
}