Vue Router doesn't update page when named variable is manually changed - vue.js

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.

Related

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')
}

VueJS router alias to particular param

I have a lot of articles in my app, and the URL are written like this in Vue Router: /article/:id.
I have particular articles I want to "pin" and have easier URLs. For example: /pinned-article, which should point to /article/3274 and /other-pinned-article, pointing to /article/68173.
I though about adding this to my routes, but it doesn't work:
{ path: '/article/3274', component: Article, alias: '/pinned-article' }
I thought about something else, involving another component:
{ path: '/pinned-article/:id', component: PinnedArticle }
The component PinnedArticle silently aliasing the correct article with a command like router.alias in the <script> section, but it apparently doesn't exist.
Is there a way to solve this problem? I thought I could use some answers I read here in Stackvoverflow (for examples when it comes to redirect /me to /user/:id, but it doesn't apply.
Thanks in advance :)
addRoute
You can achieve this with Dynamic Routing, which is not the same as dynamic route matching, i.e. route params.
(This solution works in both Vue 3 and Vue 2 with Vue Router >= 3.5.0)
By using the addRoute method of Vue router, you can create routes at runtime. You can either use a redirect or not, depending on whether you want the url bar to read /article/3274 or /pinned.
Redirect
If you want the url to change from /pinned to /article/3274, use redirect:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/pinned',
name: 'pinned',
redirect: { name: 'article', params: { id: 3274 }}
})
}
}
Access the route like:
this.$router.push('/pinned')
The above example assumes you give your Article route a name: 'article' property so you can redirect to it
Alias
You can keep the URL as /pinned using alias. Normally the alias would go on the existing Article route definition, but that doesn't work well with route params. You can use a "reverse alias" with a new route:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/params/3274',
name: 'pinned',
alias: '/pinned',
component: () => import('#/views/Article.vue') // Article component path
})
}
}
Access the route like:
this.$router.push('/pinned')
Notes:
You'll probably want to pass an id argument to the pinRoute methods rather than hardcode them like in the examples above.
A nice thing about addRoute with either method above is if the route already exists, say, from the last time you called the method, it gets overwritten. So you can use the method as many times as you like to keep changing the destination of /pinned. (The docs in both Vue 2 and Vue 3 say the route definition will get overwritten, though Vue 2 router throws a duplicate route warning.)
Of course the pinned route won't automatically persist between app refreshes, so you'll need to save/load the pinned id (i.e. using localStorage, etc.) and run one of these methods on app load if you want that

How do I detect the page the user came from in vue.js when the page opens in a new tab?

I've got an interesting problem in my vue.js application and I don't know how to solve it.
We've got a "my listings" page that shows a grid of listings that the user created. When they click on one, it takes them to the listing details page. It opens this page in a new browser tab.
What we want to do is add a new component to the top of the page that shows the user the stats on their listing. But we want this component to show up ONLY when they come to the listing details page from the My Listings page. There are other ways of getting to the Listing Details page and we don't want the stats component to show up when they come from these other ways.
I would think this could be handled in the router. I tried seeing if I could detect that the user was coming from the My Listings page from the "from" parameter in the beforeEach(...) method of the router. I did this:
router.beforeEach(async (to, from, next) => {
console.log('from=', from);
console.log('to=', to);
});
When it prints the from parameter, I get this:
to= {
fullPath: "/"
hash: ""
matched: []
meta: {}
name: null
params: {}
path: "/"
query: {}
}
It contains no information about where it came from. I'm guessing this is because it opens the Listing Details page in a new tab. So I can't use the router to tell where the user came from.
Instead, I resorted to using localStorage:
On the My Listings page:
<v-btn :href="`/listings/${listing.listingId}`" target="_blank" #click="saveFromMyListings();">View Listing</v-btn>
...
saveFromMyListings() {
localStorage.setItem('from-my-listings', true);
},
On the Listing Details page:
async created () {
this.fromMyListings = localStorage.getItem('from-my-listings') === 'true';
localStorage.setItem('from-my-listings', false);
},
So long as I set the 'from-my-listings' item in localStorage to false immediately after I use it to determine that the user came from the My Listings page, it works. That way, it is ONLY set if the user comes from the My Listings page, and never set if the user comes from anywhere else.
The problem with this method is that if the user refreshes the page, the stats disappear. Obviously, this is because created() reruns and this time 'from-my-listings' is removed from localStorage. I can fix this by not setting it to false in created() once it's used, but then where do I remove it in such a way that it's guaranteed to be removed no matter how the user leaves the page (entering a new url directly in the browser, closing the browser, computer loses power, etc.)?
Is there some other hook in vue.js besides created() that runs only once (when the user first visits the page) but not on subsequent loads (like refresh)? Is there a way to pass props to a component in the router based on the state of localStorage that won't have to be passed again on refresh? What other solutions might there be to this problem?
You could use query parameters. You'd have to change the links to something like this:
yourapp.com/listing-detail/333?from=list
then in the created function you can check window.location.search for the from value

Data passing using props in router is not working

I am trying to send data from one vue component to another by using props in router. but it is not working. whenever i try to log the props it outputs undefined. code is given below
From where data is sending
Where receiving
in index.js. router setting
None of the code you've posted matches up.
Firstly, the console logging should be just console.log(this.myprops). The point of using props is that you don't need to reference the router itself, e.g. via $router.
Next problem, you're mixing path and params. That isn't allows. See https://router.vuejs.org/guide/essentials/navigation.html. params are for named routes.
I imagine what you're aiming for is something like this:
self.$router.replace({ name: 'DashboardPatient', params: { myprops: authUser.email } })
with router config:
{
path: '/patient',
component: Dash,
children: [
{
path: ':myprops', // <--- Adding myprops to the URL
name: 'DashboardPatient',
component: DashboardPatient,
props: true,
meta: { requiresAuth: true }
}
]
}
Keep in mind that routing is all about building and parsing the URL. So the value of myprops needs to be in the URL somewhere. In my example it comes at the end, so you'll get /patient/user#example.com as the URL. If it weren't in the URL then there'd be no way for the router to populate the prop if the user hit that page directly (or refreshed the page).
To hit the same route using a path instead of a name it'd be something like this:
self.$router.replace({ path: `patient/${encodeURIComponent(authUser.email)}` })
or even just:
self.$router.replace(`patient/${encodeURIComponent(authUser.email)}`)
Personally I'd go with the named route so that the encoding is handled automatically.
If you don't want to put the data in the URL then routing is not the appropriate way to pass it along. You'd need to use an alternative, such as putting it in the Vuex store.

How to handle navigation when removing the current page's Vuex record?

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"});
}
}, ...];