Advertisement

#15 Reversing User Navigation With The Back Button

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.

Pop route payload

  • root
    • src
      • store
        • route-types.ts

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;
...
(a) Defining the pop route payload type.

Pop route state constants

  • root
    • src
      • store
        • store-constants.ts

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";
...
(b) Adding the constants that will point to the action and mutation for popping a route from the history.

The logic for popping a route

  • root
    • src
      • store
        • route-module.ts

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;
        }
    },
    ...
};
(c) Creating the action and mutation that will be responsible for popping the first element from our history array.

Navigating back

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

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();
    }
    ...
}
(d) Adding a new navigate back stream that we can use for navigating backward.

Inject the store

  • root
    • src
      • main.ts

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);
...
(e) Passing in the store when constructing the routing service.
Advertisement

Forward and backward navigation

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

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>
(f) Updating the router outlet to support both forward and backward navigation.

Notifying other components that the route has changed.

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

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();
    }
    ...
}
(g) Adding the ability for our routing service to notify other components that the route has changed.

The state constant

  • root
    • src
      • store
        • store-constants.ts

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";
(h) Exporting a constant that we can use in the state decorator for the routes state.

The conditional back button

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

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>
(i) Conditionally showing the back button based on the length of the route history array.

Set the main nav button active on backward navigation

  • root
    • src
      • components
        • main-nav
          • MainNavButton.vue

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>
(j) Subscribing to the navigate backward stream in our main nav button.
Exciton Interactive LLC
Advertisement