I'm trying to restart my component when the route changes:
beforeRouteUpdate (to, from, next) {
Object.assign(this.$data, this.$options.data())
console.log(to.query);
next();
}
but it doesn't work; it only prints the console.log. I've also tried this.$options.data.call(this) and apply.
In order to force Vue to re-render same component upon route query change, it is possible to assign a key to the <router-view it mount's into and push router to the page with the same route name or path.
Example:
Mounting point:
<router-view
:key="$route.fullPath"
/>
Component navigation, assuming route name is blog
<router-link :to={ name: 'blog', query: { count: 10 } }>Link to the same route</router-link>
This is assuming that you want to reset data of page component while navigating to the same page component with different data.
If component that you want to reset is not the route component, it is possible to reset it's data with watch option, while saving original data.
Example:
data () {
return {
initialData: {
// some initial data
},
data: {}
}
},
watch: {
'$route.fullPath': {
immediate: true, // Immediate option to call watch handler on first mount
handler () {
this.resetData()
}
}
},
methods: {
resetData () {
this.data = Object.assign({}, this.initialData)
},
},
Note, that any $route options can be watched and additional conditions added to handler via next and previous arguments.
Try
this.$router.reload()
Related
I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly.
Is there a way to wait for the data to be finished loading?
Here is how userProfile is set in Vuex:
mutations: {
setUserProfile(state, val){
state.userProfile = val
}
},
actions: {
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.teachersCollection.doc(user.uid).get()
// set user profile in state
commit('setUserProfile', userProfile.data())
},
}
Here is the code where I want to acess it:
<template>
<div>
<h1>Test</h1>
{{userProfile.firstname}}
{{institute}}
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
data() {
return {
institute: "",
}
},
computed: {
...mapState(['userProfile']),
},
created(){
this.getInstitute();
},
methods: {
async getInstitute() {
console.log(this.userProfile); //is here still empty at initial page load
const institueDoc = await this.userProfile.institute.get();
if (institueDoc.exists) {
this.institute = institueDoc.name;
} else {
console.log('dosnt exists')
}
}
}
}
</script>
Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation.
I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.
Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.
If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if
<template v-if="userProfile.id">
<!-- your normal html here... -->
</template>
<template v-else>
loading user profile...
</template>
To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:
export default {
data: () => ({
institute: ''
}),
computed: {
...mapState(['userProfile']),
},
watch: {
'userProfile.institute': {
async handler(institute) {
if (institute) {
const { name } = await institute.get();
if (name) {
this.institute = name;
}
}
},
immediate: true
}
}
}
Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.
Not specific to Vue.js but to Javascript Single Page applications. If you have a form and a rather long running submit action, like saving something. The submit operation should save something and then pushing to a new route for a success message.
While waiting for the result, the user clicks on a different link and is going away.
See this fiddle:
https://jsfiddle.net/hajbgt28/4/
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
methods: {
async submit() {
await setTimeout(() => {
this.$router.push("/bar");
}, 5000);
}
}
};
const Foo = { template: '<div>Foo</div>' }
const Bar = { template: '<div>Bar</div>' }
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
new Vue({
router,
el: '#app',
data: {
msg: 'Hello World'
}
})
Click Home
Click the button
Click on "Foo" immediately, you see "Foo"
Wait a few seconds
The Page changes to "Bar"
I have a two solutions in my mind:
I can check inside the submit operation if I am still on the route I expect and only proceed if the user is still on this page. It is rather complicated to check it every time
Disable all links on the page while loading. But this makes the page useless until the operation is finished.
What is the best practice for situations like this?
You could use a beforeRouteLeave navigation guard to abort that action (i.e., cancel the timer in your example) upon switching routes.
Assuming identifiable submit actions, save the ID of the operation result (i.e., save the timer ID from setTimeout's return value in your example).
Add a beforeRouteLeave handler to the component to cancel the submit action (i.e., clear the timer ID in your example).
const Home = {
methods: {
submit() {
this.timerId /* 1 */ = setTimeout(() => {
this.$router.push("/bar");
}, 5000);
}
},
beforeRouteLeave (to, from, next) {
clearTimeout(this.timerId) /* 2 */
next()
}
};
updated jsfiddle
Here's one idea: make a component that provides (using Vue's provide/inject API):
A function that starts an operation. This is called when a form is sent. It provides a whenDone callback which is either executed or ignored, depending on if the operation is cancelled.
A function that cancels all pending operations. The cancel function could be called when the user navigates away.
The implementation could look like this:
const CancellableOperationProvider = {
name: "CancellableOperationProvider",
props: {},
data: () => ({
pendingOperations: []
}),
/*
* Here we provide the theme and colorMode we received
* from the props
*/
provide() {
return {
$addOperation(func) {
this.pendingOperations.push(func);
func(function whenDone(callback) {
if (this.pendingOperations.includes(func)) callback();
});
},
$cancelAllOperations() {
this.pendingOperations = [];
}
};
},
render() {
return this.$slots.default[0];
}
};
The usage would look like this:
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
inject: ['$addOperation', '$cancelAllOperations'],
methods: {
async submit() {
this.$addOperation(whenDone => {
await setTimeout(() => {
whenDone(() => this.$router.push("/bar"));
}, 5000);
});
}
}
};
You could then add a navigation guard to the Vue Router so that $cancelAllOperations is called after clicking any link. Since $cancelAllOperations is only accessible through the inject API you will have to make a component that imperatively adds a navigation guard to the Vue router after mounting and removes it when unmounting.
Let me know if it doesn't work--I haven't done Vue in a while.
I used the answer from tony19 to make solution which fits my needs for use cases without setTimeout too:
const Home = {
template: '<div><button #click="submit">Save and go Bar!</button></div>',
data() {
return {
onThisPage: true
}
},
beforeRouteLeave(to, from, next) {
this.onThisPage = false;
next();
},
methods: {
submit() {
setTimeout(() => {
if (this.onThisPage) {
this.$router.push("/bar");
}
}, 5000);
}
}
};
See here: https://jsfiddle.net/ovmse1jg/
emmmmm...let me explain the situation I meet.
I have a parent component with two children that both litsen the same event and do the same thing.(codes below):
mounted() {
EventBus.$on('edit', (data) => {
console.log('service called')
this.showRightSide(data)
})
},
showRightSide(data) {
console.log(data)
// display right-side operator edit page.
this.$store.commit({
type: 'setShownState',
shown: true
})
// giving operator name & operator type
this.$store.commit({
type: 'setOptName',
optName: data.name
})
this.$store.commit({
type: 'setOptType',
optType: data.type
})
},
with the vue router below
{
path: '/main',
name: 'Main',
component: Main,
children: [
{ path: 'service', name: 'Service', component: ServiceContent },
{ path: 'model', name: 'Model', component: ModelContent }
]
},
There should be three commits during each 'edit' event, isn't it?
In fact. Firstly it has 3 commits.
But when I change from '/main/service' to '/main/model', it made 6 commits during each 'edit' event(the old ServiceContent component still made 3 commits and the new ModelContent component offers 3 commits).
when I back to '/main/service', 9 commits!!!
devtool:
It seems that when router-view changed, the component of old view can still listen the event, how can I fix it?
(EventBus is just a global vue instance used as a bus)
When you call $on(), Vue registers your callback function internally as an observer. This means your function lives on, even after the component is unmounted.
What you should do is use $off when your component is unmounted.
For example
methods: {
showRights (data) {
// etc
}
},
mounted () {
EventBus.$on('edit', this.showRights)
},
beforeDestroy () {
EventBus.$off('edit', this.showRights)
}
I would start with manually cleaning up listeners in your beforeUnmount function. Because of the way that JS works with garbage collection, I would be surprised if vue is smart enough to clean up externally reference stuff like this.
methods: {
handleEventBusEdit(data) {
console.log('service called')
this.showRightSide(data)
}
},
mounted() {
EventBus.$on('edit', this.handleEventBusEdit)
},
beforeDestroy() {
EventBus.$off('edit', this.handleEventBusEdit)
}
I am new to vue-router navigation guards and so I recently realized that I needed to use beforeRouteUpdate guard for reused components where for example: Going from /foo/1 to /foo/2
However, while coming to /foo/1, I pulled data from database through an axios call and before going to /foo/2, I need to pull new data again through the axios call.
This is where I face a problem where the navigation guard beforeRouteUpdate loads the component /foo/2 before my data loads from the axios call and thus I get null in a few of my variables.
How can I make beforeRouteUpdate wait to load the next component so that all my data is loaded from the axios calls?
As for my code, it looks like this:
beforeRouteUpdate (to, from, next) {
Vue.set(this.$store.state.user, 'get_user', null)
this.$store.dispatch(OTHER_PROFILE_GET, to.params.id).then(resp => {
console.log(resp);
if(this.$store.getters.is_user_loaded) {
next()
} else {
this.$store.watch((state, getters) => getters.is_user_loaded, () =>
{
if(this.$store.getters.is_user_loaded) {
console.log(this.$store.state.user.get_user);
console.log('comes here');
next()
}
})
}
})
},
To explain my code further, I have called this method in my component and so I when I go from /user/1 to /user/2 I dispatch a Vuex action which makes an axios call to get the new profile details but before the axios call completes and loads the data in the Vuex state, the beforeRouteUpdate already loads the next component.
First, your action should perform any state mutation such as setting user.get_user to null. I'm also not sure why you've added a watch; your action should only resolve when complete. For example
actions: {
[OTHER_PROFILE_GET] ({ commit }, id) {
commit('clearUserGetUser') // sets state.user.get_user to null or something
return axios.get(`/some/other/profile/${encodeURIComponent(id)}`).then(res => {
commit('setSomeStateData', res.data) // mutate whatever needs to be set
})
}
}
then your route guard should have something like
beforeRouteUpdate (to, from, next) {
this.$store.dispatch(OTHER_PROFILE_GET, to.params.id).then(next)
}
In order to prevent errors from trying to render null data, use your getters. For example, say your getter is
getters: {
is_user_loaded (state) {
return !!state.user.get_user
}
}
in your component, you can map this to a computed property...
computed: {
isUserLoaded () {
return this.$store.getters.is_user_loaded // or use the mapGetters helper
},
user () {
return this.$store.state.user.get_user // or use the mapState helper
}
}
then in your template, use this logic to conditionally render some data
<div v-if="isUserLoaded">
Hello {{user}}
</div>
<div v-else>
Loading...
</div>
This is the suggested approach in the vue-router guide for beforeRouteUpdate
I'm giving Vue.js a try and so far I'm loving it because it's much simpler than angular. I'm currently using vue-router and vue-resource in my single page app, which connects to an API on the back end. I think I've got things mostly working with a the primary app.js, which loads vue-router and vue-resource, and several separate components for each route.
Here's my question: How do I use props to pass global data to the child components when the data is fetched using an asynchronous AJAX call? For example, the list of users can be used in just about any child component, so I would like the primary app.js to fetch the list of users and then allow each child component to have access to that list of users. The reason I would like to have the app.js fetch the list of users is so I only have to make one AJAX call for the entire app. Is there something else I should be considering?
When I use the props in the child components right now, I only get the empty array that the users variable was initialized as, not the data that gets fetched after the AJAX call. Here is some sample code:
Simplified App.js
var Vue = require('vue');
var VueRouter = require('vue-router')
Vue.use(VueRouter);
var router = new VueRouter({
// Options
});
router.map({
'*': {
component: {
template: '<p>Not found!</p>'
}
},
'/' : require('./components/dashboard.js'),
});
Vue.use(require('vue-resource'));
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
users: [],
};
},
methods: {
fetchUsers: function() {
this.$http.get('/api/v1/users/list', function(data, status, response) {
this.users = data;
}).error(function (data, status, request) {
// handle error
});
}
}
});
router.start(App, '#app')
Simplified app.html
<div id="app" v-cloak>
<router-view users = "{{ users }}">
</router-view>
</div>
Simplified dashboard.js
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
},
};
When dashboard.js gets run, it prints an empty array to the console because that's what app.js initializes the users variable as. How can I allow dashboard.js to have access to the users variable from app.js? Thanks in advance for your help!
p.s. I don't want to use the inherit: true option because I don't want ALL the app.js variables to be made available in the child components.
I believe this is actually working and you are being misled by the asynchronous behavior of $http. Because your $http call does not complete immediately, your console.log is executing before the $http call is complete.
Try putting a watch on the component against users and put a console.log in that handler.
Like this:
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
watch: {
users: {
handler: function (newValue, oldValue) {
console.log("users is now", this.users);
},
deep: true
}
}
}
};
In the new version of Vue 1.0.0+ you can simply do the following, users inside your component is automatically updated:
<div id="app" v-cloak>
<router-view :users="users"></router-view>
</div>