In Vue.js, how do you prevent navigation for a subroute? - vue.js

The nice thing about beforeRouteLeave is that you can prevent navigating away under certain conditions.
I have a setup that uses a subroute to render part of the page. I would like a navigation guard on the subroute to prevent switching to another one if the data is not saved.
{
path: '/customers/view',
component: ViewCustomerShell,
children: [
{path: ':id', name: 'ViewCustomer', component: ViewCustomer}
]
},
So when I visit /customers/view/12 and make a change, if they try to load /customers/view/13, I want to pop up the usual confirmation and potentially stop navigation. Since beforeRouteLeave is not called in this situation, what is the recommended approach for preventing navigation? It seems that watching $route would be too late, because then the navigation has already occurred.
Note: As mentioned above, beforeRouteLeave is not called in this situation; it doesn't work.
Note: Using onbeforeunload doesn't work because it only triggers when the entire page changes.

I have also posted the same answer here.
Dynamic route matching is specifically designed to make different paths or URLs map to the same route or component. Therefor, changing the argument does not technically count as leaving (or changing) the route, therefor beforeRouteLeave rightly does not get fired.
However, I suggest that one can make the component corresponding to the route responsible for detecting changes in the argument. Basically, whenever the argument changes, record the change then reverse it (hopefully reversal will be fast enough that it gets unnoticed by the user), then ask for confirmation. If user confirms the change, then use your record to "unreverse" the change, but if the user does not confirm, then keep things as they are (do not reverse the reverse).
I have not tested this personally and therefor I do not gurantee it to work, but hopefully it would have cleared up any confusion as to which part of the app is responsible for checking what change.

I know that this post is very old. but it was the first one I found when looking for the same problem.
I have no idea if there is a better solution nowadays but for those who are looking for a solution, I can share mine:
1. Define a global state
let isRouteChangeBlocked: boolean = false;
export function blockRouteChange(set?: boolean): boolean {
if (arguments.length == 1) {
isRouteChangeBlocked = !!set;
return isRouteChangeBlocked;
}
return isRouteChangeBlocked;
}
2. Replace the route function
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location: RawLocation) {
if (blockRouteChange()) {
if (confirm("Du hast ungespeicherte Änderungen, möchtest du fortfahren?")) {
blockRouteChange(false);
return originalPush.call(this, location) as any;
}
return;
}
return originalPush.call(this, location) as any;
};
3. Set the state
#Watch("note.text")
private noteTextChanged() {
blockRouteChange(true);
}
This does exactly what I want. If nowadays there is a better solution, let me know. You can get the full runnable example here: https://github.com/gabbersepp/dev.to-posts/tree/master/blog-posts/vuejs-avoid-routes/code/example

You could use a $route object inside your component to watch if it changes and then raise up the confirmation modal... This will get called whenever your route changes!
const Baz = {
data () {
return { saved: false }
},
template: `
<div>
<p>baz ({{ saved ? 'saved' : 'not saved' }})<p>
<button #click="saved = true">save</button>
</div>
`,
watch: {
'$route': function () {
if (this.saved || window.confirm('Not saved, are you sure you want to navigate away?')) {
// do something ...
}
}
}

Related

How to stop Vue.js 3 watch() API triggering on exit

I have implemented a watch within a Vue component that displays product information. The watch watches the route object of vue-router for a ProductID param to change. When it changes, I want to go get the product details from the back-end API.
To watch the route, I do this in Product.vue:
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute();
async function getProduct(ProductID) {
await axios.get(`/api/product/${ProductID}`).then(..do something here)
}
// fetch the product information when params change
watch(() => route.params.ProductID, async (newID, oldID) => {
await getProduct(newId)
},
//watch options
{
deep: true,
immediate: true
}
)
},
}
The above code works, except that if a user navigates away from Product.vue, for example using the back button to go back to the homepage, the watch is triggered again and tries to make a call to the API using undefined as the ProductID (becaues ProductID param does not exist on the homepage route) e.g. http://localhost:8080/api/product/undefined. This causes an error to be thrown in the app.
Why does the watch trigger when a user has navigated away from Product.vue?
How can this be prevented properly? I can do it using if(newID) { await getProduct(newId) } but it seems counterintuitive to what the watch should be doing anyway.
UPDATE & SOLUTION
Place the following at the top replacing the name for whatever your route is called:
if (route.name !== "YourRouteName") {
return;
}
That will ensure nothing happens if you are not on the route you want to watch.
I ran into the same problem. Instead of watching the current route, use vue-router onBeforeRouteUpdate, which only gets called if the route changed and the same component is reused.
From https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
const userData = ref()
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
watch registers the watcher inside an vue-internal, but component-independent object. I think it's a Map. So destroying the component has no effect on the reactivity system.
Just ignore the case where newID is undefined, like you already did. But to prevent wrapping your code in a big if block just use if(newID === undefined)return; at the beginning of your callback. If your ids are always truthy (0 and "" are invalid ids) you can even use if(!newID)return;.
well, in your use case the best approach would be to have a method or function which makes the api call to the server, having watch is not a really good use of it, because it will trigger whenever route changes and you do not want that to happen, what you want is simply get the productID from route and make the api call,
so it can be done with getting the productID in the created or mounted and make the api call!

Why in my nuxt-link doesn't reload page with same url?

If I’m on a page with the URL 'http://localhost:8080/item' and I’m clicking on the same link on this page, then the page does not reload.
I need to make that if I click on the same link, the page will reload.
My link:
<nuxt-link :to="/item">
Any insight will be welcome. Thanks!
Use key, something like:
<router-view :key="$route.params.yourCustomParam"/>
Also you can use something like:
<router-link :to="{ params: { yourCustomParam: Data.now } }" replace>link</router-link>
Remember to is passed router.push() and it accept an object also. Doing that, it is more declarative and controllable. I'm using this to decide if the page of component should be rerendered since they will based on id params obtained from URL entry, and my child component can still using nesting .
I recently tried to solve a similar issue and to overcome this I used Vuex with :key (ref).
Firstly, in your store you need a state property such as:
export const state = () => ({
componentUpdates: {
item: 0,
//can add more as needed
}
})
In general, you could use only one property across the app if you prefer it that way. Just remember that later on, the key value needs to be unique - that is in the case if you used this property for two or more components within one page, for example. In this case, you could do something like this :key="$store.getters.getComponentUpdates.item+'uniqueString'"
then a getter:
export const getters = {
getComponentUpdates(state) {
return state.updateComponent;
}
}
finally a mutatation:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update]++
}
}
Now we can utilise the reactive :key wherever needed.
But first in your nuxt-link lets add an event to trigger the mutation, note the usage of #click.native to trigger the click event:
<nuxt-link #click.native="$store.commit('updateComponent', { update: 'item'})" :to="/item">
Now in the item page, for example. Let's imagine there is a component that needs to be updated. In this case we would add :key to it:
<my-item :key="$store.getters.getComponentUpdates.item" />
That is it. As you can see this solution utilises the benefits of nuxt-link but also allows us to selectively update only parts of our page that need updates (we could update the entire page this way as well if needed).
In case if you needed to trigger the logic from mounted or initial load in general, then you could use computed property and :key to your div container, right inside the <template> of your page.
Add :key to the div:
<template>
<div :key="$store.getters.getComponentUpdates.item"></div>
</template>
Create computed property:
computed: {
updateItemPage() {
//run your initial instructions here as if you were doing it in mounted then return the getter
this.initialLoadMethod()
return this.$store.getters.getComponentUpdates.item
}
}
The final touch, which is not crucial but can be implemented in order to reset the state property:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update] >= 10
? state.componentUpdates[payload.update] = 0
: state.componentUpdates[payload.update]++
}
}

How to understand vue router navigation?

I am working with this piece of code:
<script>
import router from '../router'
export default {
name: 'Page2',
data () {
return {
id: 0,
msg: 'Hey Nic Raboy'
}
},
created() {
this.id = this.$route.params.id;
},
methods: {
navigate() {
router.go(-1);
}
}
}
</script>
This is printed on the second page Template B if you will but I am confused on the navigate portion. Specifically this:
navigate() {
router.go(-1);
}
I have not worked with VueJS router before, can anyone please explain what this code section does?
Vue-router package works in the same way with the HTML5 History mode as it stated in the vue-router documentation
This method takes a single integer as parameter that indicates by how
many steps to go forwards or go backwards in the history stack,
similar to window.history.go(n).
So basically this is what you should expect from router.go()
// go back by one record, the same as history.back()
router.go(-1)
You can also learn more about HTML5 History mode from here
Have a look at the router.go(n) documentation in the section on programmatic navigation. The example explains that n can be positive or negative and indicates the number of steps in the browser's history to move.
// go forward by one record, the same as history.forward()
router.go(1)
// go back by one record, the same as history.back()
router.go(-1)
So router.go(-1) is the equivalent of hitting the "back" button in your browser.

Best practice to change the route (VueRouter) after a mutation (Vuex)

I've searched a lot, but there is no clear answer to that. Basically, what should be the best practice to automatically change a route after a mutation?
Ex: I click a button to login() -> action login that makes an http call -> mutation LOGIN_SUCCESSFUL -> I want to redirect the user to the main page $router.go()
Should I wrap the action in a Promise, and then listen to the result to call the route change from the component?
Should I do it directly from the $store?
Does vuex-router-sync helps in any way?
Thanks a lot!
The answer to this questions seems to be somewhat unclear in the Vue community.
Most people (including me) would say that the store mutation should not have any effects besides actually mutating the store. Hence, doing the route change directly in the $store should be avoided.
I have very much enjoyed going with your first suggestion: Wrapping the action in a promise, and changing the route from withing your component as soon as the promise resolves.
A third solution is to use watch in your component, in order to change the route as soon as your LOGGED_IN_USER state (or whatever you call it) has changed. While this approach allows you to keep your actions and mutations 100% clean, I found it to become messy very, very quickly.
As a result, I would suggest going the promise route.
Put an event listener on your app.vue file then emit en event by your mutation function. But I suggest you wrapping the action in a promise is good way
App.vue:
import EventBus from './eventBus';
methods: {
redirectURL(path) {
this.$router.go(path)}
},
created() {
EventBus.$on('redirect', this.redirectURL)
}
mutation:
import EventBus from './eventBus';
LOGIN_SUCCESSFUL() {
state.blabla = "blabla";
EventBus.$emit('redirect', '/dashboard')
}
As of now (mid 2018) API of Vuex supports subscriptions. Using them it is possible to be notified when a mutation is changing your store and to adjust the router on demand.
The following example is an excerpt placed in created() life-cycle hook of a Vue component. It is subscribing to mutations of store waiting for the first match of desired criteria to cancel subscriptions and adjust route.
{
...
created: function() {
const unsubscribe = this.$store.subscribe( ( mutation, state ) => {
if ( mutation.type === "name-of-your-mutation" && state.yourInfo === desiredValue ) {
unsubscribe();
this.$router.push( { name: "name-of-your-new-route" } );
}
} );
},
...
}

React Flux: How to trigger a one-time initial load after authenticating

I'm using a higher-order wrapper component as a root that accomplishes 2 things:
Verify auth (redirect to login or home)
Do the initial load to store once the auth is complete.
I'm finding it hard to do those 2 things in this one wrapper class because I can't find a way to do a one-time initial load trigger if the user is not authenticated(has no existing session)
So for example I trigger a load when there is a session with a callback:
componentWillMount: function() {
LoginStore.addChangeListener(this._onChange);
var authData = AuthAPIUtils.checkForSession();
if(authData !== null) {
WebAPIUtils.loadStores(this.onBootstrapComplete);
}
},
"this.onBootstrapComplete" is a callback that will change the wrapper state
onBootstrapComplete: function() {
console.log("5-the final callback was made - onBootstrapComplete");
//localStorage.setItem( 'gvLoggedIn', true ); This is set true in home
this.setState({
bootstrapComplete: true,
});
},
"this.state.bootstrapComplete" is passed down the child components to switch from a loading spinner to rendering the components
render: function(){
if(this.state.loggedIn) {
var childrenWithProps = React.Children.map(this.props.children,function(child) {
return React.cloneElement(child,{bootstrapComplete : this.state.bootstrapComplete})
},this);
return (
<div className="wrapper-container">
{childrenWithProps}
</div>
)
}
else {
return (
<div className="wrapper-container">
<Login />
</div>
)
}
But when there isn't a session this callback solution for a one-time trigger breaks down.
I looked hard for a solution and the best I've come up with is:
The wrapper can only listen to a "LoginStore" which should only trigger once when there is a login and logout and then use _onChange to check for a log in and trigger the loading then.
Create a handler in the wrapper class and pass it down to the Login class as a callback.
Maybe one of those solutions is just fine(let me know if so) but I wanted to make sure I'm not doing something fundamentally poor to bootstrap my app.
For me it looks like the good approach. Just one point maybe, AuthAPIUtils should be an action file (but maybe it is already) and manage the dispatching. Reading at your code I think you're already using it this way. Otherwise I think your approach
1.The wrapper can only listen to a "LoginStore" which should only trigger once when there is a login and logout and then use _onChange to check for a log in and trigger the loading then.
2.Create a handler in the wrapper class and pass it down to the Login class as a callback.
is good