Advertisement

#4 Communicating a Navigation Request to the Router Outlet

In this article we will start by creating a route entry object that initially just mimics the route config object from vue router. Once that is in place we will create a routing service that we will be able to inject into any of our views and components that need to make a routing request. This service will be our single point of communication between those views and components and a router outlet component that we will also be creating. The router outlet will be responsible for acting on the navigation request by performing the appropriate animations, such as translating the view in and and out, and actually performing the route change using the vue router.

Leading underscore

  • root
    • tslint.json

First up is a quick change to our tslint configuration so that we do not have to deal with any complaints about variable names with a leading underscore (a).

tslint.json

{
  ...,
  "rules": {
    ...,
    "variable-name": [
      true,
      "allow-leading-underscore"
    ]
  }
}
(a) Updating our tslint configuration so that we can use a leading underscore for our variable names.

Mimicking a route config object

  • root
    • src
      • components
        • routing
          • route-entry.ts

Next step in adding additional functionality that we want to the routing system is to replace the elements of the routing system that were added for us during project creation. We will start by first defining a route entry (b) that will contain all of the information that we need as well as provide a config getter that will create a route config object defined by vue router.

route-entry.ts

import Vue, { VueConstructor } from "vue";
import { RouteConfig } from "vue-router";

type VuePromiseFn = () => Promise<typeof import("*.vue")>;

export interface IRouteEntryConfig {
    component: VueConstructor<Vue> | VuePromiseFn;
    name: string;
    path: string;
}

export class RouteEntry {
    private readonly _component: VueConstructor<Vue> | VuePromiseFn;
    private readonly _name: string;
    private readonly _path: string;

    public get component() { return this._component; }
    public get config(): RouteConfig {
        return {
            component: this.component,
            name: this.name,
            path: this.path,
        };
    }
    public get name() { return this._name; }
    public get path() { return this._path; }

    constructor(config: IRouteEntryConfig) {
        this._component = config.component;
        this._name = config.name;
        this._path = config.path;
    }
}
(b) Creating a route entry class that will be the basis for our customized routing solution.

The routes enum

  • root
    • src
      • components
        • routing
          • types.ts

To avoid having magic strings in the project we will now introduce my usual solution which is the enum (c).

types.ts

export enum Routes {
    About,
    Home,
}
(c) Adding an enum will allow us to specify what route we are talking about without using magic strings.

The glue that holds it all together

  • root
    • src
      • component
        • routing
          • routing-service.ts

In order for us to indicate that we wish to navigate to a different route from a view or component and have the correct animation play before and after the navigation we will need a way to communicate that intention. To do this we will use a service (d) that will effectively be a singleton within our application.

routing-service.ts

import Vue from "vue";
import Router from "vue-router";
import { RouteEntry } from "@/components/routing/route-entry";
import { Routes } from "@/components/routing/types";

import Home from "@/views/Home.vue";

Vue.use(Router);

const home = new RouteEntry({
    component: Home,
    name: "home",
    path: "/",
});

const about = new RouteEntry({
    component: () => import(/* webpackChunkName: "about" */ "../../views/About.vue"),
    name: "about",
    path: "/about",
});

export class RoutingService {
    private readonly _routes = new Map<Routes, RouteEntry>([
        [Routes.About, about],
        [Routes.Home, home],
    ]);
    private readonly _values = Array.from(this._routes.values());
    private readonly _router = new Router({
        base: process.env.BASE_URL,
        mode: "history",
        routes: this._values.map((x) => x.config ),
    });

    public get router() { return this._router; }
}
(d) Creating a routing service that will provide a way for us to communicate routing requests between our view and components and a router outlet component.

Registering our routing service

  • root
    • src
      • main.ts

Now that we have a routing service that we want to provide to the rest of our application we only need to register it in our main file (e). By adding it to the provide object we will be able to inject it into any view or component that needs it.

main.ts

import Vue from "vue";
import App from "./App.vue";

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

import store from "./store";
import "./registerServiceWorker";

Vue.config.productionTip = false;

const routingService = new RoutingService();

new Vue({
  provide: {
    routingService,
  },
  router: routingService.router,
  store,
  render: (h) => h(App),
}).$mount("#app");
(e) Registering our routing service by including it in the provide object of our root vue instance.
Advertisement

The manager for our route requests

  • root
    • src
      • components
        • routing
          • TheRouterOutlet.vue

At this point we have a way for our views and components to communicate that a route change is needed but we do not have anything that will perform those requests. That means it is time for our router outlet template (f), script (g), and style (h).

TheRouterOutlet.vue

<template lang="pug">
router-view.router-view/
</template>
(f) The router outlet starts with just providing the router-view tag.

TheRouterOutlet.vue

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

@Component
export default class TheRouterOutlet extends Vue {
    
}
</script>
(g) Stub for the router outlet class.

TheRouterOutlet.vue

<style lang="sass" scoped>
.router-view
    height: 100%
</style>
(h) Initial styling just makes sure the router outlet takes up the entire height.

Adding the router outlet to our app

  • root
    • src
      • App.vue

Of course adding our router outlet is as simple as adding our splash screen. We first need to update our template (i) and quickly following that up by updating the class (j).

App.vue

<template lang="pug">
div#app
    SplashScreen/
    div#nav
        router-link(to="/") Home
        router-link(to="/about") About
    RouterOutlet/
</template>
(i) Replacing the default router-view tag with our new router outlet component.

App.vue

<script lang="ts">
...
import RouterOutlet from "@/components/routing/TheRouterOutlet.vue";
...
@Component({
    components: {
        RouterOutlet,
        ...,
    },
})
export default class App extends Vue {
    
}
</script>
(j) Registering the router outlet with the app.

Adding that communication ability I have been talking about

  • root
    • src
      • components
        • routing
          • RoutingService.ts

Right now we have the routing service that we can use to communicate with but it currently does not have any means for perform that communication. We are going to fix this now by using some more rxjs (k).

RoutingService.ts

...
import { Subject } from "rxjs";
...
export class RoutingService {
    private readonly _navigate = new Subject<Routes>();
    private readonly _navigate$ = this._navigate.asObservable();
    ...
    public get navigate$() { return this._navigate$; }
    ...
    public complete = () => {
        this._navigate.complete();
    }

    public navigateTo = (to: Routes) => {
        this._navigate.next(to);
    }
}
(k) Adding a navigateTo method that can be called when navigation is required and a navigate$ observable to broadcast that request out.

Exporting for easier importing

  • root
    • src
      • components
        • routing
          • index.ts

Now that we have the essentials in place we can make them easier to use by exporting them from and index file (l).

index.ts

export * from "@/components/routing/route-entry";
export * from "@/components/routing/routing-service";
export * from "@/components/routing/types";
(l) Exporting our classes and types from an index file makes them easier to import by a consumer.

Sending a route request

  • root
    • src
      • views
        • About.vue

To test that everything is working the way we want it to we can update our about view to make a navigation request when it is mounted (m).

About.vue

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

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

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

    private mounted() {
        this.routingService.navigateTo(Routes.Home);
    }
}
</script>
(m) By updating our about view we can make a route request when the view is loaded.

Is anybody listening?

  • root
    • src
      • components
        • routing
          • TheRouterOutlet.vue

Now we have a navigation request being made but currently nothing is listening for it. Making a few small changes to our router outlet we are now able to hear the requests and we will be able to perform the appropriate actions (n).

TheRouterOutlet.vue

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

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

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

    private beforeDestroy() {
        this.routingService.complete();
    }

    private created() {
        this.routingService
            .navigate$
            .subscribe(x => console.log(Routes[x]));
    }
}
</script>
(n) Updating our router outlet so that it is able to listen for navigation requests and respond appropriately.
Exciton Interactive LLC
Advertisement