Skip to content

Instantly share code, notes, and snippets.

@arnemileswinter
Last active June 9, 2020 04:00
Show Gist options
  • Save arnemileswinter/90f1d709d03724de20a484b2b93800b2 to your computer and use it in GitHub Desktop.
Save arnemileswinter/90f1d709d03724de20a484b2b93800b2 to your computer and use it in GitHub Desktop.
Vue Router Plugin to support forwards&backwards navigation
/*
Vue navigation plugin that remembers history not as a plain-old array, but properly lets you navigate through it.
This is useful for example in order to implement back-buttons.
The existing implementations I've found did not use Vue's Observable feature though, which is why I've created this,
further i did not find any implementation supporting vue-router's `replace` API.
In order for `replace` to work, you must replace calls to `$router.replace('/myRoute')` with `$navigation.replace(`/myRoute`)`.
There is also the alternative to vue-routers `push`, `$navigation.push()` but that's just for convenience.
Author: Arne Miles Winter
License: MIT
## Register the plugin with Vue:
```
import VueRouter from "vue-router";
import NavigationPlugin from "./plugins/navigation";
import router from "./routes"; // Or any other file where you declare your routes.
import {home} from "./routes"; // You can use an "index"-sort-of route if you want.
// Then users clicking on direct links will have the option to go "back" to the landing page.
// This is optional.
Vue.use(VueRouter);
Vue.use(NavigationPlugin, {router, root:home});
new Vue({
render: h => h(App),
router
}).$mount("#app");
```
## Use in your component:
```
<template>
<router-link v-if="$navigation.state.hasBack" :to="$navigation.state.back">
Back
</router-link>
<router-link v-if="$navigation.state.hasForward" :to="$navigation.state.forward">
Forward
</router-link>
</template>
```
There are also the convenience methods `this.$navigation.back()` and `this.$navigation.forward()`
*/
import Vue from "vue";
// public data to query the within your components using `this.$navigation.state.hasBack`, `this.$navigation.state.hasForward`, `...`
const state = Vue.observable({
// whether a back route is available
hasBack: false,
// whether a forward route is available
hasForward: false,
// whether the current route has been navigated before.
hasNavigatedBefore: false,
// whether the current route was navigated by going back in the history.
hasNavigatedBack: false,
// current vue-router route. This might be null on first page load, if no root is provided as an option.
current: null,
// vue-router route to return to, e.g. what history.go(-1) would yield. This is null if no navigation has occurred yet.
back: null,
// vue-router route to return to, e.g. what history.go(1) would yield. This is null if no navigation has occurred yet.
forward: null
});
// Align the plugin state without exposing implementation details.
const alignStateWithNodesHistory = (node) => {
if(!node) {
return;
}
// The state object should only countain information from Vue-Router, e.g. the 'route' objects.
state.current = node;
state.back = node.back ? node.back.route : node;
state.forward = node.forward ? node.forward.route : null;
// and some booleans for convenience
state.hasBack = node && node.back !== null && node.back.route.path !== node.route.path;
state.hasForward = node && node.forward !== null && node.forward.route.path !== node.route.path;
};
export default {
install(Vue, {router, root}) {
let replaced = false;
let currentHistoryNode = root ? {route: root, back: null, forward: null} : null;
alignStateWithNodesHistory(currentHistoryNode);
const beforeEach = (to, from, next) => {
if (currentHistoryNode === null) {
// first time navigating
currentHistoryNode = {
route: to,
back: null,
forward: null
};
} else {
// This branch is reached when router.replace('/myRoute') is called.
if (replaced) {
// Router programmatic replace.
replaced = false; // enter this branching only once per replacement.
// When the route is replaced we must set the current route to the one replaced with.
currentHistoryNode.route = to;
state.hasNavigatedBefore=false;
// And walk backwards in time to eliminate duplicates.
while (currentHistoryNode.back && currentHistoryNode.back.route.path === to.path) {
state.hasNavigatedBack=true;
currentHistoryNode.back.forward = currentHistoryNode.forward;
currentHistoryNode = currentHistoryNode.back;
}
// And walk forwards in time to eliminate duplicates.
while (currentHistoryNode.forward && currentHistoryNode.forward.route.path === to.path) {
state.hasNavigatedBefore=true;
currentHistoryNode.forward.back = currentHistoryNode.back;
currentHistoryNode = currentHistoryNode.forward;
}
alignStateWithNodesHistory(currentHistoryNode);
next();
return;
}
// These branches are reached when router.push('/myRoute') is called or user navigates.
if (currentHistoryNode.back && currentHistoryNode.back.route.path === to.path) {
// If the back route has the 'to' path, this is a backwards navigation.
currentHistoryNode.back.forward = currentHistoryNode;
currentHistoryNode = currentHistoryNode.back;
state.hasNavigatedBefore=true;
state.hasNavigatedBack=true;
} else if (currentHistoryNode.forward && currentHistoryNode.forward.route.path === to.path) {
// This is a forward navigation to a previously visited route.
currentHistoryNode = currentHistoryNode.forward;
state.hasNavigatedBefore=true;
state.hasNavigatedBack=false;
} else {
// otherwise a forward navigation to a previously unvisited route
currentHistoryNode = {
route: to,
back: {...currentHistoryNode},
forward: null
};
state.hasNavigatedBefore=false;
state.hasNavigatedBack=false;
}
}
alignStateWithNodesHistory(currentHistoryNode);
next();
};
const $navigation = {
state,
replace: (...params) => {
replaced = true;
return router.replace(...params);
},
push: (...params) => router.push(...params),
back: () => {
if (state.hasBack) {
router.push(currentHistoryNode.back.route);
}
},
forward: () => {
if (state.hasForward) {
router.push(currentHistoryNode.forward.route);
}
}
};
router.beforeEach(beforeEach);
Vue.prototype.$navigation = $navigation;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment