Vue Router route query param added by beforeEnter lost after navigate into the same route - vue.js

When navigating into the same route you are currently at, the route query page added by the before enter route guard is lost.
This is due to the fact that router link to object does not contain query page, and it does not go into before enter hook anymore, since it is already in the same route.
I figured out that you can add it in your router push or link button and you need to add this whenever you know the view contain query page.
Example:
this.$router.push({ name: 'routeName', query: { page: 1 } });
Question:
Is there an elegant way to handle this in route guard?
Which hook should I use so that route query page can be kept even user navigate into the same route?
Example code:
Route
// Sample route
const routes = [
{
path: 'test',
name: 'Test',
component: TestPage,
beforeEnter: testPageGuard,
},
];
Route Guard
// Test Page Guard
testPageGuard: (to, from, next) => {
const { page = null } = to.query;
let finalNext;
if (!page) {
finalNext = {
...to,
query: {
...to.query,
page: 1,
},
};
}
if (finalNext) {
next(finalNext);
} else {
next();
}
}
View
// TestPage.vue
<template>
<!-- The problem can be reproduce when clicking this link
when you are already in route '/test' -->
<router-link :to="{ name: 'Test'}">
Test
</router-link>
</template>
<script>
export default {
name: 'Test',
};
</script>
Solution Figured:
Add query page to router link
// TestPage.vue
<template>
<!-- query page is added here -->
<router-link :to="{ name: 'Test', query: { page: 1 } }">
Test
</router-link>
</template>
<script>...</script>

I figured out that there are two more ways to do this.
Solution 1: Update at "beforeRouteUpdate" Hook
beforeRouteUpdate triggers when query param changes even when in the same route.
Hence we can remove the beforeEnter guard and the extra page query in the route link, and do query param page adding at that particular page.
Example Code
View
// TestPage.vue
<template>
<!-- The problem can be reproduce when clicking this link
when you are already in route '/test' -->
<router-link :to="{ name: 'Test'}">
Test
</router-link>
</template>
<script>
export default {
name: 'Test',
// Solution here
beforeRouteUpdate(to, from, next) {
if (!Object.prototype.hasOwnProperty.call(to.query, 'page')) {
next({
...to,
query: {
// This line is used to retain other query if there is any
...to.query,
page: 1,
},
});
} else {
next();
}
},
};
</script>
Solution 2: Update at "beforeEach" Hook
When query param changes even when in the same route, it actually go through beforeEach hook also.
Hence we can remove the beforeEnter guard and the extra page query in the route link.
Add meta tag hasQueryParamPage for that route and do query param page adding in the global beforeEach hook.
This design has better reusability if you have other pages that require the query param page.
Example Code
Route
// Sample route
const routes = [
{
path: 'test',
name: 'Test',
component: TestPage,
// add meta tag
meta: { hasQueryParamPage: true },
},
];
Router
// router.js
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
...
});
// Solution here
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => (record.meta.hasQueryParamPage))) {
let updatedNext = null;
if (!Object.prototype.hasOwnProperty.call(to.query, 'page')) {
updatedNext = {
...to,
query: {
// This line is used to retain other query if there is any
...to.query,
page: 1,
},
};
}
if (updatedNext) {
next(updatedNext);
return;
}
}
next();
});

Related

How to achieve "visited recently" using Vue?

I'm trying to create a list of hyperlinks to the last 5 pages (views) visited by a user. However I don't really know where/ how to start...
I thought about somehow storing each route when visited but I don't know what's the best practice to do so.
I'm not asking for a working code, I just need some help to find the right direction to start with.
You can use router.beforeEach((to, from, next) =>{}) and store the url in an array. You can read more details about navigation guards here.
https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards
Found this, it may help:
https://codesandbox.io/s/mutable-glade-qnrxm
It stores the visited routes.
It works like this:
<template>
<div>
<span v-for="crumb in crumbs" :key="crumb.index">
<router-link :to="crumb.path">{{ crumb.name }}</router-link> |
</span>
</div>
</template>
<script>
export default {
data() {
return {
crumbs: []
};
},
watch: {
$route(to) {
if (this.crumbs.length > 3) {
this.crumbs.shift();
}
this.crumbs.push({
path: to.fullPath,
name: to.name/* */
});
}
},
mounted() {
this.crumbs.push({
path: this.$route.fullPath,
name: this.$route.name
});
}
};
</script>
Here, it gets the route name and path on mount and pushes it to crumbs array.
mounted() {
this.crumbs.push({
path: this.$route.fullPath,
name: this.$route.name
});
}
Then it constantly watches and changes on route. On change, it shifts(removes the last and adds a new one) any router if there are more than 3. And then it assignes the name and path for every new router.
watch: {
$route(to) {
if (this.crumbs.length > 3) {
this.crumbs.shift();
}
this.crumbs.push({
path: to.fullPath,
name: to.name/* */
});
}
},
And at last, it loops the crumbs and displays it.
<span v-for="crumb in crumbs" :key="crumb.index">
<router-link :to="crumb.path">{{ crumb.name }}</router-link> |
</span>

Dynamic Vue Router

I am researching whether a vue router is the best approach for the following scenario:
I have a page containing 'n' number of divs. Each of the divs have different content inside them. When a user clicks on a button in the div, I would like the div to open in a separate browser window (including its contents).
Can a route name/component be created on the fly to route to? Since I have 'n' number of divs, that are created dynamically, I cannot hard-code name/components for each one
<router-link :to="{ name: 'fooRoute'}" target="_blank">
Link Text
</router-link>
I want to avoid the same component instance being used (via route with params) since I may want multiple divs to be open at the same time (each one in their own browser window)
If the link is opening in a separate window, it makes no sense to use a <router-link> component as the application will load from scratch in any case. You can use an anchor element instead and generate the href property dynamically for each div.
To answer your questions:
A route name cannot be created dynamically since all routes must be defined at the beginning, when the app (along with router) is being initialized. That said, you can have a dynamic route and then dynamically generate different paths that will be matched by that route.
There is no way for the same component instance to be reused if it's running in a separate browser window/tab.
It is possible to create dynamic router name.
profileList.vue
<template>
<main>
<b-container>
<b-card
v-for="username in ['a', 'b']"
:key="username"
>
<b-link :to="{ name: profileType + 'Profile', params: { [profileType + 'name']: username }}">Details</b-link>
</b-container>
</main>
</template>
<script>
export default {
name: 'profileList',
data () {
return {
profileType: ''
}
},
watch: {
// Call again the method if the route changes.
'$route': function () {
this.whatPageLoaded()
}
},
created () {
this.whatPageLoaded()
},
methods: {
whatPageLoaded () {
this.profileType = this.$route.path // /user or /place
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
</style>
b-container, b-card, b-link are taken from bootstrap-vue, so you can freely change it.
router.js
const router = new Router({
mode: 'hash',
base: process.env.BASE_URL,
linkExactActiveClass: 'active',
routes: [
// USERS
{
path: '/user/:username',
name: userProfile,
component: userProfile
},
{
path: '/user',
name: 'userList',
component: profileList
},
// PLACES
{
path: '/place/:placename',
name: placeProfile,
component: placeProfile
},
{
path: '/place',
name: 'placeList',
component: ProfileList
}
]
})

VueJs use props that comes from <router-link>

i have a navbar and there is a text field in that the user can search for posts by tags. If the user enters 1-3 tags, the written tags will be stored in a tags array.
My router-link in the navbar component looks like this: (only relevant part)
<router-link :to="{name:'posts', props:{searchTags: tags}}">
<button type="button" v-if="this.tags.length > 0"
class="...">Search
</button>
</router-link>
in my routes.js is my posts route (important snippet of my routes.js)
routes: [
{
path: "/posts",
component: posts,
name: 'posts'
},
]
The navbar should send the tags array to the posts component. Unfortunately I can't do it.
The posts component, sends a post request to an API that gets the latest posts. But I want that when tags are passed, not the newest posts are fetched, only posts with certain tags. But first I have to get the tags somehow.
I tried to get them with "this.$props.searchTags" and other things. Unfortunately the result is always "undefined".
export default {
name: "posts",
props: {
searchTags: Array,
required: false
},
data: function () {
return {
apiUrl: '/getPosts',
....
tags: [this.searchTags],
}
},
methods: {
getPosts: function (url) {
this.$http.get(url).then(function (data) {
// blabla
});
},
getPostsByTags: function() {
//
},
},
created() {
if(this.$props.searchTags == null)
this.getPosts(this.apiUrl);
else
this.getPostsByTags(bla);
},
}
Router link to property accepts string or Location as a value. Location object does not have props property.
Instead, it is possible to use params to send data to route component:
<router-link
:to="{ name: 'posts', params: { searchTags: tags } }"
>
...
</router-link>
This way searchTags with value of assigned tags will be accessible via this.$route.params.searchTags inside destination component.
So created hook of example above should be updated to:
created () {
if (!this.$route.params.searchTags) {
this.getPosts(this.apiUrl);
} else {
this.getPostsByTags(bla);
}
},
Try to add props: true in your route definition
routes: [
{
path: "/posts",
component: posts,
name: 'posts',
props: true
},
]

VueJS vue-router passing a value to a route

In VueJS 2 with vue-router 2 I am using a parent view with subcomponents like this:
WidgetContainer.vue with route /widget_container/:
<template>
<component :is="activeComponent"></component>
</template>
<script>
import WidgetA from './components/WidgetA'
import WidgetB from './components/WidgetB'
export default {
name: 'WidgetContainer',
components: {
WidgetA,
WidgetB
},
data () {
return {
activeComponent: 'widget-a'
}
}
}
</script>
In WidgetA I have the option of selecting a widget id
<template>
// v-for list logic here..
<router-link :to="{ path: '/widget_container/' + widget.id }"><span>{{widget.name}} </span></router-link>
</template>
<script>
export default {
name: 'WidgetA',
data() {
return {
widgets: [
{ id: 1,
name: 'blue-widget'
}]}}
routes.js:
export default new Router({
routes: [
{
path: '/widget_container',
component: WidgetContaner
},
{
path: '/widget_container/:id?',
redirect: to => {
const { params } = to
if (params.id) {
return '/widget_contaner/:id'
} else {
return '/widget_container'
}
}
}]})
From the WidgetContainer if the route is /widget_container/1 (where '1' is the id selected in WidgetA) I want to render WidgetB, but I cant work out:
1) how to pass the selected widget id into the router-link in WidgetA
2) How to know in WidgetContainer the the route is /widget_contaner/1 instead of /widget_container/ and render WidgetB accordingly.
Any ideas?
You can pass data to parent using by emitting event, you can see more details around here and here.
Once the data is change, you can watch over it and update the variable which has stored your widget.
Another option, if communication between components become unmanageable over time is to use some central state management, like vuex, more details can be found here.
Wouldn't it be easier and more scallable to use Vuex for that?
Just commit id to store and than navigate ?

Navigating vuejs SPA via routes that share component does not refresh component data as expected

I have a couple routes in my vuejs SPA that I have set up using vue-router:
/create/feedback
/edit/feedback/66a0660662674061b84e8ea2fface0e4
The component for each route is the same form with a bit of smarts to change form values based on the absence or present of the ID in the route (feedbackID, in my example).
I notice that when I click from the edit route to the create route, the data in my form does not clear.
Below is the gist of my route file
import FeedbackFormView from './components/FeedbackForm.vue'
// Routes
const routes = [
{
path: '/create/feedback',
component: FeedbackFormView,
name: 'FeedbackCreate',
meta: {
description: 'Create Feedback',
}
},
{
path: '/edit/feedback/:feedbackId',
component: FeedbackFormView,
name: 'FeedbackEdit',
meta: {
description: 'Edit Feedback Form'
},
props: true
}
]
export default routes
Below is the gist of my component
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
}
}
</script>
However, if I modify my component as follows, everything works as expected
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
},
watch: {
'$route' (to, from) {
if (to.path === '/create/feedback') {
this.model = {}
}
}
}
}
</script>
Why is this? Why do I need watch?
I would have though that changing routes would be sufficient as the purpose of routing is to mimic the semantic behavior of page navigation
You have same component for different routes, when you go to edit route from the create route component is already created and mounted so the state of the component doesn't clear up.
Your component can listen to route changes using $router provided by vue-router every time the route changes the watcher is called.
For those who come this later, the following answer addresses the issue I was facing:
Vue-Router: view returning to login page after page refresh