#8 Making Our App Bar Back Button Do Something
Sunday, July 28, 2019
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.
Parts
- Part 45: Adjusting Shares
- Part 44: Plan Percentages
- Part 43: Home Securities
- Part 42: Updating Plans
- Part 41: Plan Details View
- Part 40: Portfolio Getters
- Part 39: Portfolio Plan
- Part 38: Portfolio Category
- Part 37: Account Securities
- Part 36: Account Transfer
- Part 35: View Account Security
- Part 34: Updating Deposit
- Part 33: View Account Deposit
- Part 32: Display Account Details
- Part 31: Account Getters
- Part 30: Deposits And Securities
- Part 29: Add Accounts Details
- Part 28: Refactoring Accounts
- Part 27: Add Security Models
- Part 26: Edit Security Details
- Part 25: View Security Details
- Part 24: Navigating To Details
- Part 23: Getters Validation
- Part 22: Query Parameters
- Part 21: Tab Entries
- Part 20: Tab Header
- Part 19: List View
- Part 18: Vuex Getters
- Part 17: End Domain Model
- Part 16: Start Domain Model
- Part 15: Pop Routes
- Part 14: Push Routes
- Part 13: Removing Accounts
- Part 12: Vuex (Decorators)
- Part 11: Vuex (Accounts)
- Part 10: The App Bar (Settings)
- Part 9: Remove Consumer Rxjs
- Part 8: The App Bar (Back)
- Part 7: Structuring Our App
- Part 6: Animation Between Views
- Part 5: Navigation Fade
- Part 4: Navigation Requests
- Part 3: Fade Animations (cont.)
- Part 2: Fade Animations
- Part 1: Splash Screen
Moving the app bar file
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
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>
Change the app
- root
- src
- App.vue
- src
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>
For now what does go back mean?
- root
- src
- components
- routing
- route-entry.ts
- routing
- components
- src
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;
}
}
Remove the map
- root
- src
- components
- routing
- routing-service.ts
- routing
- components
- src
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);
...
}
}
Now we can go back
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
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>
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>
There is a problem brewing
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
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>
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
- app-bar
- components
- src
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>
This is 2019 we are too cool for callbacks
- root
- src
- components
- routing
- routing-service.ts
- routing
- components
- src
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; }
...
}
Enter async/await
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
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>
What is the current route?
- root
- src
- components
- routing
- routing-service.ts
- routing
- components
- src
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;
}
...
}
Hiding the back button
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
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>
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>
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>