Update the document title when navigating to a new route
This commit is contained in:
parent
4a0b6d80e4
commit
32803e0ea1
|
@ -8,7 +8,7 @@
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
|
||||||
<title>Vite App</title>
|
<title>Freifunk Knotenverwaltung</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
@ -2,13 +2,12 @@ import { createApp } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router, { initTitleSync } from "./router";
|
||||||
import { useConfigStore } from "@/stores/config";
|
import { useConfigStore } from "@/stores/config";
|
||||||
import { useVersionStore } from "@/stores/version";
|
import { useVersionStore } from "@/stores/version";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
|
@ -18,6 +17,8 @@ async function main() {
|
||||||
await configLoaded;
|
await configLoaded;
|
||||||
await versionLoaded;
|
await versionLoaded;
|
||||||
|
|
||||||
|
initTitleSync();
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
createWebHistory,
|
createWebHistory,
|
||||||
type LocationQueryRaw,
|
type LocationQueryRaw,
|
||||||
type RouteLocationNormalized,
|
type RouteLocationNormalized,
|
||||||
|
type RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
||||||
import AdminNodesView from "@/views/AdminNodesView.vue";
|
import AdminNodesView from "@/views/AdminNodesView.vue";
|
||||||
|
@ -23,12 +24,26 @@ import {
|
||||||
type TypeGuard,
|
type TypeGuard,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { parseJSON } from "@/shared/utils/json";
|
import { parseJSON } from "@/shared/utils/json";
|
||||||
|
import { useConfigStore } from "@/stores/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route to use for navigation.
|
||||||
|
*/
|
||||||
export interface Route {
|
export interface Route {
|
||||||
|
/**
|
||||||
|
* Name of the route to navigate to.
|
||||||
|
*/
|
||||||
name: RouteName;
|
name: RouteName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional query parameters. This is a flat mapping from key to value. Nested values must be stringified already.
|
||||||
|
*/
|
||||||
query?: LocationQueryRaw;
|
query?: LocationQueryRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum used to identify each route. Each field corresponds to `name` field of one of the routes below.
|
||||||
|
*/
|
||||||
export enum RouteName {
|
export enum RouteName {
|
||||||
HOME = "home",
|
HOME = "home",
|
||||||
NODE_CREATE = "node-create",
|
NODE_CREATE = "node-create",
|
||||||
|
@ -37,6 +52,13 @@ export enum RouteName {
|
||||||
ADMIN_NODES = "admin-nodes",
|
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 {
|
export function route(name: RouteName, query?: LocationQueryRaw): Route {
|
||||||
return {
|
return {
|
||||||
name,
|
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>(
|
function getQueryField<T>(
|
||||||
route: RouteLocationNormalized,
|
route: RouteLocationNormalized,
|
||||||
field: string,
|
field: string,
|
||||||
|
@ -56,6 +87,15 @@ function getQueryField<T>(
|
||||||
return isT(value) ? value : undefined;
|
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>(
|
function getJSONQueryField<T>(
|
||||||
route: RouteLocationNormalized,
|
route: RouteLocationNormalized,
|
||||||
field: string,
|
field: string,
|
||||||
|
@ -77,46 +117,110 @@ function getJSONQueryField<T>(
|
||||||
return isT(json) ? json : undefined;
|
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({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
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),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
export default router;
|
||||||
|
|
Loading…
Reference in a new issue