Advertisement

#22 Adding Support To The Routing System For Query Parameters

In this article we will add the ability for our routing system to handle query parameters. As it stands now all of the routing that we have wanted to perform can be controlled by simply passing a single enum value around. There are at least two different ways that we can approach the problem of adding CRUD operations to our application. One being transmitting the data that we need between views via the store and the other by transmitting the data through query parameters. We are going to opt for using query parameters. So by the time we are done we will no longer use a single enum value for routing but instead we will use a route object that will contain the enum value and query parameters, as well as a couple of other properties. We will finish up by adding a method for extracting query parameters from the query string in a type safe manner.

Code Snippets

TheAppBarSettings.vue (2:18)

<script lang="ts">
...
export default class TheAppBarSettings extends Vue {
    ...
    private created() {
        ...
        this.routingService
            .navigateBack$
            .subscribe(this.close);
    }
    ...
}
</script>
src ⟩ components ⟩ app-bar ⟩ TheAppBarSettings.vue

TheAppBar.vue (2:45)

<script lang="ts">
...
export default class TheAppBar extends Vue {
    @Inject() private readonly routingService!: RoutingService;
    @State(STATE_ROUTES) private readonly routeState!: IRouteState;

    private get showBack() { return this.routeState.history.length !== 0; }

    private back() {
        this.routingService.back();
    }
</script>
src ⟩ components ⟩ app-bar ⟩ TheAppBar.vue

types.ts (4:22)

import { Dictionary } from "vue-router/types/router";
...
export interface IRouteOptions {
    query?: Dictionary<string | Array<string | null>>;
}

export interface IRoute extends IRouteOptions {
    id: Routes;
    name: string;
}
src ⟩ components ⟩ routing ⟩ types.ts

route-types.ts (5:55)

import { IRoute } from "@/components/routing";

export interface IRouteState {
    history: IRoute[];
}
...
export type PushRoutePayload = IRoute;
src ⟩ store ⟩ route-types.ts

routing-service.ts (6:26)

...
import {
    IRoute,
    IRouteOptions,
    Routes,
} from "@/components/routing/types";
...
export class RoutingService {
    private readonly _navigate = new Subject<IRoute>();
    private readonly _navigateBack = new Subject<IRoute>();
    ...
    private readonly _navigateBack$ = this._navigateBack.asObservable();
    ...
    public get history() { return this._store.state[STATE_ROUTES].history; }
    public get navigateBack$() { return this._navigateBack$; }
    ...
    public back = () => {
        if (this.history.length === 0) {
            return;
        }
        const to = this.history[0];
        ...
    }
    ...
    public createRoute = (to: Routes, options?: IRouteOptions): IRoute => {
        return {
            id: to,
            name: this.find(to).name,
            ...options,
        };
    }
    ...
    public navigateTo = (to: Routes, options?: IRouteOptions) => {
        if (this.current.isSameRoute(to)) {
            return;
        }
        if (this.history.length > 0 && this.history[0].id === to) {
            this.back();
            return;
        }
        this._navigate.next(this.createRoute(to, options));
        this._routeChanged.next();
    }

    public queryString = (route: IRoute) => {
        return typeof(route.query) !== "undefined"
            ? `?${Object.keys(route.query)
                    .map((x) => `${x}=${route.query![x]}`)
                    .join("&")}`
            : "";
    }
    ...
}
src ⟩ components ⟩ routing ⟩ routing-service.ts

Advertisement

MainNavButton.vue (11:56)

<script lang="ts">
...
import { IRoute, ... } from "@/components/routing";
...
export default class MainNavButton extends Vue {
    ...
    private setActive(route: IRoute) {
        this.isActive = this.route === route.id;
    }
}
</script>
src ⟩ components ⟩ main-nav ⟩ MainNavButton.vue

TheRouterOutlet.vue (12:31)

<script lang="ts">
...
import {
    IRoute,
    IRouteOptions,
    RouteEntry,
    Routes,
    RoutingService,
} from "@/components/routing";
...
export default class TheRouterOutlet extends Vue {
    ...
    private toRoute!: IRoute;
    ...
    private animate(route: IRoute, ...) {
        ...
        this.toEntry = this.routingService.find(route.id);
        this.toRoute = route;
        ...
    }

    private animateBack(route: IRoute) {
        ...
    }

    private animateForward(route: IRoute) {
        ...
    }
    ...
    private animationCompleteOut() {
        ...
        this.$router.push(`${this.toEntry.path}${this.routingService.queryString(this.toRoute)}`);
        ...
    }
    ...
    private navigateBack(route: IRoute) {
        ...
    }

    private navigateForward(route: IRoute) {
        const current = this.routingService.current;
        this.pushRoute({
            id: current.route,
            name: current.name,
            query: this.$route.query,
        });
        ...
    }
}
</script>
src ⟩ components ⟩ routing ⟩ TheRouterOutlet.vue

SecuritiesDetails.vue (15:59)

<template lang="pug">
div Securities Details
</template>
src ⟩ views ⟩ SecuritiesDetails.vue

SecuritiesDetails.vue (16:07)

<script lang="ts">
import { Component, Inject, Vue } from "vue-property-decorator";

import { RoutingService } from "@/components/routing";

@Component
export default class SecuritiesDetails extends Vue {
    @Inject() private readonly routingService!: RoutingService;
    
    private mounted() {

    }
}
</script>
src ⟩ views ⟩ SecuritiesDetails.vue

types.ts (17:17)

...
export enum Routes {
    ...
    SecuritiesDetails,
}
...
src ⟩ components ⟩ routing ⟩ types.ts

routing-service.ts (17:38)

...
const securitiesDetails = new RouteEntry({
    component: () => import(/* webpackChunkName: "securities-details" */ "../../views/SecuritiesDetails.vue"),
    name: "securities-details",
    parent: securities,
    path: "/securities-details",
    route: Routes.SecuritiesDetails,
});

export class RoutingService {
    ...
    private readonly _routes = [
        ...
        securitiesDetails,
    ];
...
}
src ⟩ components ⟩ routing ⟩ routing-service.ts

routing-service.ts (21:00)

...
export class RoutingService {
    ...
    public queryParam<Q, R extends string | number = string>(
        func: (q: Q) => string, transform: (x: string) => R = (x) => x as R) {
        const param = func((this.router.currentRoute.query as unknown) as Q);
        return transform(param);
    }
    ...
}
src ⟩ components ⟩ routing ⟩ routing-service.ts

SecuritiesDetails.vue (23:40)

<script lang="ts">
...
interface IQuery {
    id: string;
    which: string;
}
...
export default class SecuritiesDetails extends Vue {
    ...
    private mounted() {
        console.log(this.$router.currentRoute.query);
        const id = this.routingService.queryParam<IQuery, number>((x) => x.id, parseInt);
        const which = this.routingService.queryParam<IQuery>((x) => x.which);

        console.log(`${typeof(id)} ${id}`);
        console.log(`${typeof(which)} ${which}`);
    }
}
</script>
src ⟩ views ⟩ SecuritiesDetails.vue

Exciton Interactive LLC
Advertisement