Update the document title when navigating to a new route
This commit is contained in:
parent
4a0b6d80e4
commit
32803e0ea1
3 changed files with 146 additions and 41 deletions
|
@ -8,7 +8,7 @@
|
|||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
|
||||
<title>Vite App</title>
|
||||
<title>Freifunk Knotenverwaltung</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -2,13 +2,12 @@ import { createApp } from "vue";
|
|||
import { createPinia } from "pinia";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import router, { initTitleSync } from "./router";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
import { useVersionStore } from "@/stores/version";
|
||||
|
||||
async function main() {
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
|
@ -18,6 +17,8 @@ async function main() {
|
|||
await configLoaded;
|
||||
await versionLoaded;
|
||||
|
||||
initTitleSync();
|
||||
|
||||
app.mount("#app");
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
createWebHistory,
|
||||
type LocationQueryRaw,
|
||||
type RouteLocationNormalized,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
||||
import AdminNodesView from "@/views/AdminNodesView.vue";
|
||||
|
@ -23,12 +24,26 @@ import {
|
|||
type TypeGuard,
|
||||
} from "@/types";
|
||||
import { parseJSON } from "@/shared/utils/json";
|
||||
import { useConfigStore } from "@/stores/config";
|
||||
|
||||
/**
|
||||
* Route to use for navigation.
|
||||
*/
|
||||
export interface Route {
|
||||
/**
|
||||
* Name of the route to navigate to.
|
||||
*/
|
||||
name: RouteName;
|
||||
|
||||
/**
|
||||
* Optional query parameters. This is a flat mapping from key to value. Nested values must be stringified already.
|
||||
*/
|
||||
query?: LocationQueryRaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum used to identify each route. Each field corresponds to `name` field of one of the routes below.
|
||||
*/
|
||||
export enum RouteName {
|
||||
HOME = "home",
|
||||
NODE_CREATE = "node-create",
|
||||
|
@ -37,6 +52,13 @@ export enum RouteName {
|
|||
ADMIN_NODES = "admin-nodes",
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to construct a {@link Route} object for navigation.
|
||||
*
|
||||
* @param name - Name of the route to navigate to.
|
||||
* @param query - Optional query parameters. This is a flat mapping from key to value. Nested values must be
|
||||
* stringified already.
|
||||
*/
|
||||
export function route(name: RouteName, query?: LocationQueryRaw): Route {
|
||||
return {
|
||||
name,
|
||||
|
@ -44,6 +66,15 @@ export function route(name: RouteName, query?: LocationQueryRaw): Route {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the query parameter `field` in a typesafe manner.
|
||||
*
|
||||
* @param route - The route object to get the query parameter from.
|
||||
* @param field - The name of the query parameter to get.
|
||||
* @param isT - Type guard to check the query parameter has the expected type `<T>`.
|
||||
* @returns The query parameter of type `<T>`. If the query parameter is not set or has an unexpected type `undefined`
|
||||
* will be returned.
|
||||
*/
|
||||
function getQueryField<T>(
|
||||
route: RouteLocationNormalized,
|
||||
field: string,
|
||||
|
@ -56,6 +87,15 @@ function getQueryField<T>(
|
|||
return isT(value) ? value : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get JSON from the query parameter `field` in a typesafe manner.
|
||||
*
|
||||
* @param route - The route object to get the query parameter from.
|
||||
* @param field - The name of the query parameter to get.
|
||||
* @param isT - Type guard to check the JSON for the query parameter has the expected type `<T>`.
|
||||
* @returns The query parameter of type `<T>`. If the query parameter is not set, the value cannot be parsed a JSON the
|
||||
* JSON does not match the expected type `undefined` will be returned.
|
||||
*/
|
||||
function getJSONQueryField<T>(
|
||||
route: RouteLocationNormalized,
|
||||
field: string,
|
||||
|
@ -77,46 +117,110 @@ function getJSONQueryField<T>(
|
|||
return isT(json) ? json : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type to make the compiler enforce setting a title for each route.
|
||||
*/
|
||||
type RouteWithTitle = RouteRecordRaw & {
|
||||
meta: {
|
||||
/**
|
||||
* Title to set for the HTML document for the route.
|
||||
*/
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* All route definitions go here.
|
||||
*/
|
||||
const routes: RouteWithTitle[] = [
|
||||
{
|
||||
path: "/",
|
||||
name: RouteName.HOME,
|
||||
meta: {
|
||||
title: "Willkommen",
|
||||
},
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/node/create",
|
||||
name: RouteName.NODE_CREATE,
|
||||
meta: {
|
||||
title: "Neuen Knoten anmelden",
|
||||
},
|
||||
component: NodeCreateView,
|
||||
props: (route) => ({
|
||||
hostname: getQueryField(route, "hostname", isHostname),
|
||||
fastdKey: getQueryField(route, "key", isFastdKey),
|
||||
mac: getQueryField(route, "mac", isMAC),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/node/delete",
|
||||
name: RouteName.NODE_DELETE,
|
||||
meta: {
|
||||
title: "Knoten löschen",
|
||||
},
|
||||
component: NodeDeleteView,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: RouteName.ADMIN,
|
||||
meta: {
|
||||
title: "Admin - Dashboard",
|
||||
},
|
||||
component: AdminDashboardView,
|
||||
},
|
||||
{
|
||||
path: "/admin/nodes",
|
||||
name: RouteName.ADMIN_NODES,
|
||||
meta: {
|
||||
title: "Admin - Knoten",
|
||||
},
|
||||
component: AdminNodesView,
|
||||
props: (route) => ({
|
||||
filter: getJSONQueryField(route, "filter", isNodesFilter) || {},
|
||||
searchTerm: getQueryField(route, "q", isSearchTerm),
|
||||
sortDirection: getQueryField(route, "sortDir", isSortDirection),
|
||||
sortField: getQueryField(route, "sortField", isNodeSortField),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: RouteName.HOME,
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/node/create",
|
||||
name: RouteName.NODE_CREATE,
|
||||
component: NodeCreateView,
|
||||
props: (route) => ({
|
||||
hostname: getQueryField(route, "hostname", isHostname),
|
||||
fastdKey: getQueryField(route, "key", isFastdKey),
|
||||
mac: getQueryField(route, "mac", isMAC),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/node/delete",
|
||||
name: RouteName.NODE_DELETE,
|
||||
component: NodeDeleteView,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: RouteName.ADMIN,
|
||||
component: AdminDashboardView,
|
||||
},
|
||||
{
|
||||
path: "/admin/nodes",
|
||||
name: RouteName.ADMIN_NODES,
|
||||
component: AdminNodesView,
|
||||
props: (route) => ({
|
||||
filter: getJSONQueryField(route, "filter", isNodesFilter) || {},
|
||||
searchTerm: getQueryField(route, "q", isSearchTerm),
|
||||
sortDirection: getQueryField(route, "sortDir", isSortDirection),
|
||||
sortField: getQueryField(route, "sortField", isNodeSortField),
|
||||
}),
|
||||
},
|
||||
],
|
||||
routes,
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the HTML documents title for the given route.
|
||||
*
|
||||
* @param route - Route to set the title for.
|
||||
*/
|
||||
function updateTitle(route: RouteLocationNormalized) {
|
||||
const baseTitle = `${
|
||||
useConfigStore().getConfig.community.name
|
||||
} - Knotenverwaltung`;
|
||||
|
||||
let title: string;
|
||||
if (hasOwnProperty(route.meta, "title") && isString(route.meta.title)) {
|
||||
title = `${baseTitle} - ${route.meta.title}`;
|
||||
} else {
|
||||
console.error(`Missing title for route: ${route.path}`);
|
||||
title = baseTitle;
|
||||
}
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize keeping the HTML document's title in sync with the current route.
|
||||
*
|
||||
* Note: This must be called after the config store is available.
|
||||
*/
|
||||
export function initTitleSync() {
|
||||
router.beforeEach(updateTitle);
|
||||
|
||||
// Make sure the title is up-to-date after page load.
|
||||
updateTitle(router.currentRoute.value);
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
|
Loading…
Reference in a new issue