#15 Reversing User Navigation With The Back Button
Saturday, September 14, 2019
In this article we add the ability to pop a route off of the route history array and then use that ability to change the way our back button functions. Up until now we have used the parent/child relationship between our route entries to determine where to go when the user presses the back button. Now that we have a route history that we can push and pop entries from we can follow the route changes that the user has performed when navigating backwards. With the basic functionality in place we will finish up by conditionally showing the back button if the history is not empty and update the way our main navigation buttons are set as active so that they are properly highlighted for both forward and backward navigation.
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
Pop route payload
- root
- src
- store
- route-types.ts
- store
- src
Although it may be overkill in this particular case, since we do not need any additional information to pop a route off the history array, we are still going to define a pop route payload type (a).
route-types.ts
...
export type PopRoutePayload = void;
...
Pop route state constants
- root
- src
- store
- store-constants.ts
- store
- src
Every time we need to define an action and mutation for our store we first need to add the constants that will point to both (b).
store-constants.ts
...
export const ACTION_POP_ROUTE = "ACTION_POP_ROUTE";
...
export const MUTATION_POP_ROUTE = "MUTATION_POP_ROUTE";
...
The logic for popping a route
- root
- src
- store
- route-module.ts
- store
- src
As usual the action for popping a route is just directing the payload, in this case there isn't a payload, to the appropriate mutation (c). The mutation will then take in to consideration the length of the current history array and determine what needs to happen in order to remove the first element, if any, from the array.
route-module.ts
...
import {
ACTION_POP_ROUTE,
...
MUTATION_POP_ROUTE,
...
} from "@/store/store-constants";
...
export const actions: StoreActionTree = {
[ACTION_POP_ROUTE](this: Store<IStoreState>, { commit }: StoreContext) {
commit(MUTATION_POP_ROUTE);
},
...
};
export const mutations: StoreMutationTree = {
[MUTATION_POP_ROUTE](state: IStoreState) {
switch (state.routes.history.length) {
case 0:
return;
case 1:
state.routes.history = [];
return;
default:
state.routes.history = state.routes.history.slice(1);
return;
}
},
...
};
Navigating back
- root
- src
- components
- routing
- routing-service.ts
- routing
- components
- src
Up until now there has not been a notion of the 'type' of navigation so we have only had the
navigateTo
method on our routing service. With the
introduction of a history we need to distinguish between 'forward' and 'backward' navigation.
To do this we are going to make use of a new navigate back stream
(d). Just like with the forward navigation case
we will prevent backward navigation give a specific condition. In the backward case that
condition is the history is empty. In order to have access to the history we will pass in
the store to our service in the constructor.
routing-service.ts
...
import { Store } from "vuex";
...
import { IStoreState } from "@/store";
...
export class RoutingService {
...
private readonly _navigateBack = new Subject<Routes>();
private readonly _navigateBack$ = this._navigateBack.asObservable();
...
public get navigateBack$() { return this._navigateBack$; }
...
constructor(private readonly _store: Store<IStoreState>) {
}
...
public back = () => {
if (this._store.state.routes.history.length === 0) {
return;
}
const to = this._store.state.routes.history[0];
this._navigateBack.next(to);
}
...
public complete = () => {
...
this._navigateBack.complete();
}
...
}
Inject the store
- root
- src
- main.ts
- src
The change that we made in relation to the constructor of our routing service means we need to provide the store as an argument when constructing it. Luckily the store is available to use when we construct the routing service in our main file (e).
main.ts
...
const routingService = new RoutingService(store);
...
Forward and backward navigation
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
Now we are in a position to update our router outlet. The main changes that we are making is to create the methods to support both forward and backward navigation as well as changing the way we decide which animations to run (f). Instead of picking the animations based on the parent/child relationship between the to and from routes we just need to know if we are navigating forwards or backwards.
TheRouterOutlet.vue
<script lang="ts">
...
import {
ACTION_POP_ROUTE,
...
PopRoutePayload,
...
} from "@/store";
...
export default class TheRouterOutlet extends Vue {
@Action(ACTION_POP_ROUTE) private readonly popRoute!: ActionFn<PopRoutePayload>;
...
private animate(route: Routes, type: AnimationTypes) {
this.isAnimatingOut = true;
this.toEntry = this.routingService.find(route);
this.animationSubject.next(type, this.animationOptionsOut);
}
private animateBack(route: Routes) {
this.inAnimation = AnimationTypes.TranslateInFromLeft;
this.animate(route, AnimationTypes.TranslateOutToRight);
}
private animateForward(route: Routes) {
this.inAnimation = AnimationTypes.TranslateInFromRight;
this.animate(route, AnimationTypes.TranslateOutToLeft);
}
...
private created() {
this.routingService
.navigate$
.subscribe(this.navigateForward);
this.routingService
.navigateBack$
.subscribe(this.navigateBack);
}
private navigateBack(route: Routes) {
this.animateBack(route);
this.popRoute();
}
private navigateForward(route: Routes) {
this.pushRoute(this.routingService.current.route);
this.animateForward(route);
}
}
</script>
Notifying other components that the route has changed.
- root
- src
- routing
- routing-service.ts
- routing
- src
In order for us to be able to conditionally show the back button in our app bar we need a way of notifying the app bar component that the route has changed. The streams that we have available to us right now will not tell us that the route has changed they only tell us the route is changing. We need to be notified after the route history has been updated. To accomplish this we will introduce a new stream (g).
routing-service.ts
...
export class RoutingService {
...
private readonly _routeChanged = new Subject<void>();
private readonly _routeChanged$ = this._routeChanged.asObservable();
...
public get routeChanged$() { return this._routeChanged$; }
...
public back = () => {
...
this._routeChanged.next();
}
public complete = () => {
...
this._routeChanged.complete();
}
...
public navigateTo = (to: Routes) => {
...
this._routeChanged.next();
}
...
}
The state constant
- root
- src
- store
- store-constants.ts
- store
- src
Just like with the accounts state we need to define a constant that we can use when binding to the routes state (h).
store-constants.ts
...
export const STATE_ROUTES = "routes";
The conditional back button
- root
- src
- components
- app-bar
- TheAppBar.vue
- app-bar
- components
- src
Now that we have the ability to notify the app bar that the route has changed we can use that notification as a que to check and see if the history is empty or not (i). If it is not empty then we will show the back button otherwise we will not.
TheAppBar.vue
<script lang="ts">
...
import {
IRouteState,
STATE_ROUTES,
State,
} from "@/store";
...
export default class TheAppBar extends Vue {
...
@State(STATE_ROUTES) private readonly routeState!: IRouteState;
...
private created() {
this.routingService
.routeChanged$
.subscribe(this.routeChanged);
}
private routeChanged() {
this.showBack = this.routeState.history.length !== 0;
}
}
</script>
Set the main nav button active on backward navigation
- root
- src
- components
- main-nav
- MainNavButton.vue
- main-nav
- components
- src
Last up for this article is to be able to set the is active flag on our main nav buttons when the back button is pressed (j).
MainNavButton.vue
<script lang="ts">
...
export default class MainNavButton extends Vue {
...
private async created() {
...
this.routingService
.navigateBack$
.subscribe(this.setActive);
}
...
}
</script>