Instantly share code, notes, and snippets.
Last active
June 9, 2020 04:00
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save arnemileswinter/90f1d709d03724de20a484b2b93800b2 to your computer and use it in GitHub Desktop.
Vue Router Plugin to support forwards&backwards navigation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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