From 9a0eb5a49f153bceb453eb48d5ce277ff4bc8b15 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Sun, 22 Dec 2019 09:03:49 +0000 Subject: [PATCH] Set document title on change of route --- src/routing/Router.ts | 39 ++++++++++++++++++++++++++++-------- src/routing/interfaces.d.ts | 17 ++++++++++++++++ tests/routing/unit/Router.ts | 25 +++++++++++++++++++++-- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/routing/Router.ts b/src/routing/Router.ts index 34c921371..20a3e999a 100644 --- a/src/routing/Router.ts +++ b/src/routing/Router.ts @@ -1,5 +1,15 @@ +import global from '../shim/global'; import Evented from '../core/Evented'; -import { RouteConfig, History, OutletContext, Params, RouterInterface, Route, RouterOptions } from './interfaces'; +import { + RouteConfig, + History, + OutletContext, + Params, + RouterInterface, + Route, + RouterOptions, + MatchType +} from './interfaces'; import { HashHistory } from './history/HashHistory'; import { EventObject } from '../core/Evented'; @@ -11,7 +21,7 @@ interface RouteWrapper { route: Route; segments: string[]; parent?: RouteWrapper; - type?: string; + type: MatchType; params: Params; } @@ -155,7 +165,7 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl private _register(config: RouteConfig[], routes?: Route[], parentRoute?: Route): void { routes = routes ? routes : this._routes; for (let i = 0; i < config.length; i++) { - let { path, outlet, children, defaultRoute = false, defaultParams = {} } = config[i]; + let { path, outlet, children, defaultRoute = false, defaultParams = {}, title } = config[i]; let [parsedPath, queryParamString] = path.split('?'); let queryParams: string[] = []; parsedPath = this._stripLeadingSlash(parsedPath); @@ -166,6 +176,7 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl outlet, path: parsedPath, segments, + title, defaultParams: parentRoute ? { ...parentRoute.defaultParams, ...defaultParams } : defaultParams, children: [], fullPath: parentRoute ? `${parentRoute.fullPath}/${parsedPath}` : parsedPath, @@ -236,14 +247,15 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl route, segments: [...segments], parent: undefined, - params: {} + params: {}, + type: 'index' })); let routeConfig: RouteWrapper | undefined; let matchedRoutes: RouteWrapper[] = []; while ((routeConfig = routeConfigs.pop())) { const { route, parent, segments, params } = routeConfig; let segmentIndex = 0; - let type = 'index'; + let type: MatchType = 'index'; let paramIndex = 0; let routeMatch = true; if (segments.length < route.segments.length) { @@ -285,7 +297,7 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl } let matchedOutletName: string | undefined = undefined; - let matchedRoute: any = matchedRoutes.reduce((match: any, matchedRoute: any) => { + let matchedRoute: RouteWrapper | undefined = matchedRoutes.reduce((match: any, matchedRoute: any) => { if (!match) { return matchedRoute; } @@ -300,8 +312,19 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl matchedRoute.type = 'error'; } matchedOutletName = matchedRoute.route.outlet; + const title = this._options.setDocumentTitle + ? this._options.setDocumentTitle({ + outlet: matchedOutletName, + title: matchedRoute.route.title, + params: matchedRoute.params, + queryParams: this._currentQueryParams + }) + : matchedRoute.route.title; + if (title) { + global.document.title = title; + } while (matchedRoute) { - let { type, params, parent, route } = matchedRoute; + let { type, params, route } = matchedRoute; const matchedOutlet = { id: route.outlet, queryParams: this._currentQueryParams, @@ -315,7 +338,7 @@ export class Router extends Evented<{ nav: NavEvent; outlet: OutletEvent }> impl if (!previousMatchedOutlet || !matchingParams(previousMatchedOutlet, matchedOutlet)) { this.emit({ type: 'outlet', outlet: matchedOutlet, action: 'enter' }); } - matchedRoute = parent; + matchedRoute = matchedRoute.parent; } } else { this._matchedOutlets.errorOutlet = { diff --git a/src/routing/interfaces.d.ts b/src/routing/interfaces.d.ts index 40b4fd70e..85f864b8f 100644 --- a/src/routing/interfaces.d.ts +++ b/src/routing/interfaces.d.ts @@ -22,6 +22,7 @@ export interface Route { fullQueryParams: string[]; defaultParams: Params; score: number; + title?: string; } /** @@ -33,6 +34,7 @@ export interface RouteConfig { children?: RouteConfig[]; defaultParams?: Params; defaultRoute?: boolean; + title?: string; } /** @@ -159,6 +161,20 @@ export interface OnChangeFunction { (path: string): void; } +/** + * Document title option + */ +export interface DocumentTitleOptions { + title?: string; + outlet: string; + params: Params; + queryParams: Params; +} + +export interface SetDocumentTitle { + (options: DocumentTitleOptions): string | undefined; +} + /** * Options for a history provider */ @@ -200,4 +216,5 @@ export interface RouterOptions { window?: Window; base?: string; HistoryManager?: HistoryConstructor; + setDocumentTitle?: SetDocumentTitle; } diff --git a/tests/routing/unit/Router.ts b/tests/routing/unit/Router.ts index 5ae371f2b..9196c3d3e 100644 --- a/tests/routing/unit/Router.ts +++ b/tests/routing/unit/Router.ts @@ -1,6 +1,8 @@ -const { describe, it } = intern.getInterface('bdd'); +const { it } = intern.getInterface('bdd'); +const { describe: jsdomDescribe } = intern.getPlugin('jsdom'); const { assert } = intern.getPlugin('chai'); +import global from '../../../src/shim/global'; import { Router } from '../../../src/routing/Router'; import { MemoryHistory as HistoryManager } from '../../../src/routing/history/MemoryHistory'; @@ -156,7 +158,7 @@ const config = [ } ]; -describe('Router', () => { +jsdomDescribe('Router', () => { it('Navigates to current route if matches against a registered outlet', () => { const router = new Router(routeConfig, { HistoryManager }); const context = router.getOutlet('home'); @@ -543,4 +545,23 @@ describe('Router', () => { assert.strictEqual(historyManagerCount, 1); assert.isTrue(initialNavEvent); }); + + it('should set the title as defined in the routing config', () => { + const router = new Router([{ outlet: 'foo', path: 'foo/{id}?{query}', title: 'foo' }], { + HistoryManager + }); + router.setPath('/foo/id-value?query=queryValue'); + assert.strictEqual(global.document.title, 'foo'); + }); + + it('should set the title as using the set document title callback', () => { + const router = new Router([{ outlet: 'foo', path: 'foo/{id}?{query}', title: 'foo' }], { + HistoryManager, + setDocumentTitle({ title, params, queryParams, outlet }) { + return `${title}-${outlet}-${params.id}-${queryParams.query}`; + } + }); + router.setPath('/foo/id-value?query=queryValue'); + assert.strictEqual(global.document.title, 'foo-foo-id-value-queryValue'); + }); });