How to handle navigation when removing the current page's Vuex record? - vue.js

I have a ClientManagePage where I display client information and allow for the removal of the displayed client.
The vue-router route configuration for that page looks like this:
{
path: '/client/:id/manage',
name: 'client',
component: ClientManagePage,
props: ({ params }) => ({ id: params.id }),
}
The client entities are stored in a vuex store. ClientManagePage gets its client entity from the store using the id prop and displays various properties of the client and a "remove" button.
The remove button listener is (inside a mapActions):
async removeClientClicked(dispatch) {
// Wait for the action to complete before navigating to the client list
// because otherwise the ClientListPage might fetch the client list before
// this client is actually deleted on the backend and display it again.
await dispatch('removeClientAction', this.id);
this.$router.push({ name: 'clientList' });
},
The vuex action that removes a client is:
async function removeClientAction({ commit }, id) {
// Remove the client from the store first (optimistic removal)
commit('removeClient', id);
// Actually remove the client on the backend
await api.remove('clients', id);
// Moving "commit('removeClient', id);" here still produces the warning mentioned below
}
My problem is how to handle navigating to the other route when removing a client. The current code produces warnings in development mode such as:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
found in
---> <ClientManagePage> at src/pages/ClientManagePage.vue
<Root>
This is of course caused by the reactivity system kicking in and trying to update the content of the page with the now-deleted vuex client entity. This happens before the removeClientAction is completed therefore the navigation to the ClientList page.
I've come up with some possible solutions to this, but they are not very appealing:
Have a v-if="client" at the top of the ClientManagePage that hides everything while the client does not exist in the store.
Use the computed property client in ClientManagePage to return a default "dummy" client that contains the required properties for the page. The page will still flash with "fake" content while the action is underway though.
Navigate to "clientList" right after (or even before) dispatching removeClientAction. This causes the clientList to display the removed client briefly while the action completes which is not good.
Are there other solutions to this seemingly common problem of navigating away when deleting the underlying vuex entity displayed on the current page?

I ended up doing a big v-if at the top of the ClientManagePage that hides everything while the client does not exist in the store. It's not pretty, but it works. An improvement could be to display a "please wait, operation in progress" in v-else.

One option is to externalize the deletion of the record. There are a number of ways to do that, but the simplest for me was to create a new route, /records/delete/:id, and place a route guard on that route that triggers the removal. Then redirect to the records list where you wanted to go in the first place. Something along the lines of:
import store from "wherever/your/store/is";
const routes = [{
path: "/records/delete/:id",
name: "deleteRecord",
props: true,
beforeEnter: (to, from, next) => {
store.dispatch("DELETE_RECORD", to.params.id).then(() => console.log("record deleted!"));
next({name: "some/path/you/wanted/to/go/to"});
}
}, ...];

Related

Vue Router doesn't update page when named variable is manually changed

I have the following definition in my Vue router
{
path: '/search/entity/:entity',
name: PageType.SEARCH_RESULTS_ENTITY_ID,
component: Entity
}
If a user types in id3 into a search box, they end up at /#/search/entity/id3 either shows the data or a "Not Found" exception.
The challenge I'm facing is that if one of my users manually changes the URL to /#/search/entity/id4 the page stays on 3 and doesn't update to 4's content. The only way to get that to happen is with a full page refresh.
I tried this hack
window.addEventListener("hashchange", (e: HashChangeEvent) => {
router.push({
path: window.location.hash.substr(1),
});
})
but it gives this error:
NavigationDuplicated: Avoided redundant navigation to current location: "/search/entity/id4
How can I get Vue to detect and update the page when these changes happen? Note that I have this schema on a large number of pages, so I'm looking for a global project solution.

Is there anyway to ignore the *failure: Avoided redundant navigation to current location* error using Vue Router and just refresh the page?

I see this question has been asked a few times on here, but none of the answers have really helped me in this current situation.
I have an app I'm working on with a sidebar with tabs that link to different dashboards. Each of the SidebarLinks are a router-link with the to key being fed the route prop from the main component.
Inside one of these dashboards, the Analysis dashboard, there is another router that routes you to child routes for specific Analyses with their own ids (EX: /analysis/1).
The user clicks on a button for a specific analysis and they are routed to a page containing that information, on the same page.
The Error
When I click the Analysis SidebarLink the route in the url changes back to /analysis, but the page doesn't update/refresh.
I don't get an error in the console, but I do get the failure in the devtools.
I understand that Vue Router doesn't route back to a route you are already on, but I need it to. If you refresh the page when the url is just /analysis it routes back to it's inital state.
Is there anyway to refresh when it rereoutes to /analysis? Or a way to handle this error to work as intended?
What I've tried
I've tried changing the router-link to an <a> tag and programatically use router.push and then catch the error, but that doesn't do anything.
I've tried checking if the route.fullPath.contains("/analysis") and then just do router.back() but that doesn't seem to work either.
SidebarLink router function
function goToRoute() {
console.log(`route.fullPath → `, route.fullPath)
if (route.fullPath.match('/analysis*') as any) {
console.log('route includes /analysis')
router.back()
} else {
console.log('route doesnt inclue /analysis')
router
.push({
path: props.route,
})
.catch(() => {})
}
}
Inital /analysis Page
This is what the page looks like normally
/analysis/1 Page
This is what the route to analysis/1 looks like (url changes)
/analysis/1 Page When Issue Analysis SidebarLink Clicked
This is what the route to analysis looks like when the sidebarlink is clicked (url changes, but the page stays the same)
I suspect you are fetching your data from a backend service or data files
If yes you can refetch the data everytime the route param changed by watching it.
watch: {
'$route.params.id': function (id) {
if(id)
this.$store.dispatch('fetchOneAnalys', id)
else
this.$store.dispatch('fetchAllAnalyses')
}

Pagination is not working as expected with vue router

I am pushing query parameter for page change it works fine:
getProducts(){
this.$router
.push({
name: 'products',
query: {
page: this.page,
},
})
.catch(() => {})
... fetching data from backend
}
It works fine when I just click pagination items and data is loading correctly but when I click back from browser query param is geting changed but pagination and data doesn't can't changed because this.page value remains the same. How can be this fixed?
You will have to add a befoureRouteUpdate(to, from, next) hook in your component - or a watcher on $route. Vue-Router reuses the same component because you are not changing the route - only the query parameters.
Keep in mind that:
the beforeRouteUpdate will be called only when the component is being reused - not when the route is visited for the first time (for the latter you need beforeRouteEnter)
the watcher will be also called when you leave the route (e.g. if you go to clients page)

Why does the browser display cached Vue.js view on route/url change?

I have a homepage with <router-link> tags to views. It is a simple master/detail relationship where the Homepage is a catalogue of products and the Product detail page/view shows information on each item.
When I first launch the website and click on an item on the Homepage view (e.g. URL: http://localhost:8080/100-sql-server-2019-licence), the Product view gets loaded and the product detail loads fine.
If I then press the back button in the browser to return to the Homepage and then click on a different Product (e.g. URL: http://localhost:8080/101-oracle-12c-licence), the URL in the browser address bar changes but I get the previous product's information. Its lightning quick and no new network calls are done which means its just showing a cached page of the previous product. If I then hit the refresh button while on that page, the network call is made and the correct product information is displayed.
I did a search online but couldn't find this problem described on the search results. Could anyone point me in the right direction of how to cause a refresh/re-render of a route when the route changes?
What is happening
vue-router will cache your components by default.
So when you navigate to the second product (that probably renders the same component as the first product), the component will not be instantiated again for performance reasons.
From the vue-router documentation:
For example, for a route with dynamic params /foo/:id, when we
navigate between /foo/1 and /foo/2, the same Foo component instance
will be reused.
The easy (but dirty) fix
The easy -but hacky and not recommended - way to solve this is to give your <router-view /> a key property, e.g.:
<router-view :key="$route.fullPath" />
This will force vue-router to re-instantiate the view component every time the url changes.
However you will loose all performance benefits you would normally get from the caching.
Clean fix: properly handling route changes
The clean way to solve this problem is to react to the route-change in your component (mostly this boils down to moving ajax calls from mounted into a $route watcher), e.g.:
<script>
export default {
data() {
return {
productDetails: null,
loading: false
};
},
watch: {
'$route': {
// with immediate handler gets called on first mount aswell
immediate: true,
// handler will be called every time the route changes.
// reset your local component state and fetch the new data you need here.
async handler(route) {
this.loading = true;
this.productDetails = null;
try {
// example for fetching your product data
const res = await fetch("http://give.me.product.data/" + encodeURIComponent(route.params.id));
this.productDetails = await res.json();
} finally {
this.loading = false;
}
}
}
}
};
</script>
Alternative: Navigation Guards
Alternatively you could also use vue-routers In-Component Navigation Guards to react to route changes:
<script>
export default {
async beforeRouteUpdate (to, from, next) {
// TODO: The route has changed.
// The old route is in `from`, the new route in `to`.
this.productData = await getProductDataFromSomewhere();
// route will not change before you haven't called `next()`
next();
}
};
</script>
The downside of the navigation guards is that you can only use them directly in the component that the route renders.
So you can't use navigation guards in components deeper within the hierarchy.
The upside is that the browser will not view your site before you call next(), which gives you time to load the data necessary before your route is displayed.
Some helpful ressources
Vue Router Navigation Guards Documentation
vue-router github issue
Similar Question about vue-router component reuse on stackoverflow

how do I bring up "Are you sure you want to leave page?" popup in vue.js router?

I'm building a vue.js application. We'd like to have a popup come up when the user attempts to leave a specific page. The popup should say "Are you sure you want to leave the page?" I know I can implement something in the beforeRouteLeave hook of the component, but I'm wondering if there's a way to implement this in the beforeEach event of the router (i.e. not the component). The reason I'd like to use the router is because beforeEach in the router seems to respond to the user entering a different path in the browser url bar, whereas the beforeRouteLeave hook on the component does not. However, I don't have access to the popup in the router whereas I do in the component (the popup would just be part of the template).
So the question is: how can I bring up a popup in the router before the user actually leaves the page?
Thanks.
First you can assign a name for each of your routes objects in routes array inside your router or another field like requiredConfirmation or something like that, imagine that we have a routes like this :
routes : [
{
path : '/needconfirm',
component : NeedConfirmToLeaveCom,
name : 'needconfirm-route1'
},
{
path : '/neednotconfirm',
component : NeedNotConfirmToLeaveCom,
name : 'normal-route1'
},
]
then you can use router.beforeEach to set some conditions or some confirmations based on your Origin route and Destination route.
something like this :
router.beforeEach((to,from,next) => {
if(from.name.startsWith("needconfirm-")) {
if(window.confirm("Are you sure you want to leave the page?")) {
next();
}
}else next();
});
UPDATE * :
if you want to use some custom components for your popup, you can use vuex to store your component's logic and toggler and import that component in your App.Vue or other root/child components you wish. because you have access to your store management using $store right?
UPDATE ** :
and one other thing i want to mention, if you want to save some progress or state and because of that you want to get confirmation from user (progress will lost if they switch route), you should consider using Vuex to store your progress or state of your application and if you want more persisted solution you can use VuexPersisted store management which uses LocalStorage.
Vue router navigation guards document
Vuex Doc
You should use beforeunload event listener on the main component in that view.
MDN Reference
Depending on the browser, it will show the popup with default values populated.
This is how I use it in the created hook of the main component
window.addEventListener("beforeunload", (e) => {
e.preventDefault()
// chrome requires returnValue to be set
const message = "You have unsaved changes. Are you sure you wish to leave?"
e.returnValue = message
return message
})