Advertisement

#8 Making Our App Bar Back Button Do Something

In this article we will focus on adding some functionality to our app bar by allowing the user to go back by pressing our angle left button. In the current context since we have not set up a mechanism for dealing with global state going back means heading from our current location towards the root view by way of the parent of our routes. Once that is done we will overcome a small unexpected problem of determing whether or not to show the back button through some asynchronous programming by using async/await.

Moving the app bar file

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

First things first we are going to start by simply changing the location of our app bar vue file. And now that we have nested the file one level lower that it was previously we just need to update the import within our style tag (a).

TheAppBar.vue

<style lang="sass" scoped>
@import "../../bourbon/bourbon"
...
</style>
(a) Updating the import in our style tag due to the change in location of the vue file.

Change the app

  • root
    • src
      • App.vue

Of course we are creating the app bar component within our app so we still do need to update the import statement there as well (b).

App.vue

<script lang="ts">
import AppBar from "@/components/app-bar/TheAppBar.vue";
...
</script>
(b) Updating the import in our app component.

For now what does go back mean?

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

Before we add any global state management to our application we can take a first crack at what it means if a user wants to go back. To do this we are going to modify how we approach creating routes and what information they know. The first things we are going to do is to update our route entries so that they are aware of what route enum they are associated with (c). While we are at it we are also going to publicly expose the route entry parent as well.

route-entry.ts

...
import { Routes } from "./types";

export interface IRouteEntryConfig {
    ...
    route: Routes;
}

export class RouteEntry {
    ...
    private readonly _route: Routes;
    ...
    public get parent() { return this._parent; }
    ...
    public get route() { return this._route; }

    constructor(config: IRouteEntryConfig) {
        ...
        this._route = config.route;
    }
}
(c) Updating our route entry so that it is aware of the route it is associated with.

Remove the map

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

Now that each of our route entries are aware of their associated route we can change the internal implementation of our routing service away from using a map and just use an array (d). With that done we can add a back method which will just move us from the current route to its parent until we reach the root of our application.

routing-service.ts

...
const home = new RouteEntry({
    ...,
    route: Routes.Home,
});

const about = new RouteEntry({
    ...,
    route: Routes.About,
});
...
export class RoutingService {
    ...
    private readonly _routes = [
        about,
        home,
    ];

    private readonly _values = Array.from(this._routes.values()); // <-- remove this
    private readonly _router = new Router({
        ...,
        routes: this._routes.map((x) => x.config),
    });
    ...
    public back = () => {
        const parent = this.current.parent;
        if (parent === null) {
            return;
        }
        this.navigateTo(parent.route);
    }
    ...
    public find = (route: Routes) => {
        const entry = this._routes.find((x) => x.route === route);
        ...
    }
    ...
    private entryByName = (name: string) => {
        const route = this._routes.find((x) => x.name === name);
        ...
    }
}
(d) Removing the map from our routing service and replacing it with a simple array.

Now we can go back

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

Here we make a change to our template so that when a user presses the angle left button we invoke the back callback which we will create next (e). Again we will also take this opportunity to correct a typo that I made previously.

TheAppBar.vue

<template lang="pug">
div.app-bar
    button.angle-left(v-on:click.prevent="back")
        FontAwesomeIcon(
            icon="angle-left"
            size="lg")/
    //- Misspelled 'Portfolio' previously
    div.title Portfolio Balancer
    button.cog
        FontAwesomeIcon(icon="cog")/
</template>
(e) Adding a callback to our angle left button that will move the user up the route tree.

Of course since we have already done the work in our routing service we just need to call its back method (f).

TheAppBar.vue

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

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

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

    private back() {
        this.routingService.back();
    }
}
</script>
(f) Updating our app bar script to invoke the back method of the routing service that we created previously.
Advertisement

There is a problem brewing

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

If the user is already on the home screen it would be nice if we indicated in some manner that you can not go any farther back. The way that we will do this is by not showing the button if you are on the home screen. This seems like a pretty simple thing to do until we make a simple check. We will start by just logging the current route (g).

TheAppBar.vue

<script lang="ts">
...
export default class TheAppBar extends Vue {
    ...
    private created() {
        console.log(this.routingService.current);
    }
}
</script>
(g) Console logging the current route just to see what it says.

To start with everything seems to be working just fine. And everything will work fine unless/until you refresh the browser while you are not on the home page. Once this happens we are suddenly create with our app telling us that we are on the root url and that the name of our route is set to null. I am of the belief that this problem is a development time problem which is caused by how the test server is set up. Just to be on the safe side though we will solve this problem assuming that it can/will happen in production.

Waiting for the router to be ready

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

Luckily for us the vue router does provide us a mechanism for interacting with it once it has indicated that it is ready (h).

TheAppBar.vue

<script lang="ts">
...
export default class TheAppBar extends Vue {
    ...
    private created() {
        this.$router.onReady(() => {
            console.log(this.routingService.current);
        });
    }
}
</script>
(h) Using the on ready callback of the vue router will solve our problem.

This is 2019 we are too cool for callbacks

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

Now using the callback would of course be perfectly fine but it would be even cooler to use async/await instead. To do this we will just wrap the on ready method in a promise (i).

routing-service.ts

...
export class RoutingService {
    ...
    private readonly _currentPromise = new Promise<RouteEntry>((resolve, _) => {
        this._router.onReady(() => resolve(this.current));
    });
    ...
    public get currentAsync() { return this._currentPromise; }
    ...
}
(i) Wrapping the on ready callback of the vue router in a promise.

Enter async/await

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

As a quick test lets just update our console log statement to make use of our new asynchronous capabilities (j).

TheAppBar.vue

<script lang="ts">
...
export default class TheAppBar extends Vue {
    ...
    private async created() {
        console.log(await this.routingService.currentAsync);
    }
}
</script>
(j) Updating our console log statement to use async/await instead of the on ready callback.

What is the current route?

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

In order to hide our back button we are going to need to know what the current route is. Which of course sounds like something the routing service should be able to tell us (k).

routing-service.ts

...
export class RoutingService {
    ...
    public async isCurrentRouteAsync(route: Routes) {
        const current = await this.currentAsync;
        return current.route === route;
    }
    ...
}
(k) Adding the ability for our routing service to tell us if the route specified is the current route.

Hiding the back button

  • root
    • src
      • components
        • app-bar
          • TheAppBar.vue

With all of this in place we can now determine whether or not the back button should be hidden. We will start by just adding the v-if directive to our template (l).

TheAppBar.vue

<template lang="pug">
div.app-bar
    button.angle-left(
        v-if="showBack"
        ...)
        ...
    ... 
    ...
</template>
(l) Adding the v-if directive to the back button in our template.

We are finally in a position to show/hide our back button depending on the current route and what route we are currently navigating to (m). We may at some point want to have the ability to be notified when routing as ended but for now this will work.

TheAppBar.vue

<script lang="ts">
...
import { ..., Routes } from "@/components/routing";
...
export default class TheAppBar extends Vue {
    ...
    private showBack = false;
    ...
    private async created() {
        this.showBack = !await this.routingService.isCurrentRouteAsync(Routes.Home);
        
        this.routingService
            .navigate$
            .subscribe(this.routeChange);
    }

    private routeChange(route: Routes) {
        this.showBack = route !== Routes.Home;
    }
}
</script>
(m) Updating our app bar to show/hide the back button based on the page when created and the page the user is navigating to.

Last thing we will do is fix a little bit of jittery behavior. If you pay attention to the title of our app when the button is hidden and shown you will notice that there is a little bit of movement. This is the result of the buttons not having the same native height. We can fix this by adjusting the minimum height of the smaller one (n).

TheAppBar.vue

<style lang="sass" scoped>
...
@import "../../bitters/functions"
...
.angle-left, .cog
    ...
    @include padding(0.75rem 1rem) // <-- extra space between the '1' and 'rem'
    min-height: rem(45.33px)
</style>
(n) Setting the min height of our buttons to prevent the jumping around of our title.
Exciton Interactive LLC
Advertisement