Vuex: How to grab latest user data after user profile updated? - vue.js

I am using Vuex and having trouble getting a user's data to be "reactive" after his profile has been updated. Here's my scenario:
My App.vue checks a user's properties during the created() hook
like so:
async created() {
await this.$store.dispatch('getSSOUser') // gets user from auth server
await this.$store.dispatch('fetchUserProfiles') // queries known user table to see if user has a profile
// set for global user state throughout app
await this.setUser()
// then loads the UI
this.isBusy = false
methods: {
setUser() {
const user = this.getUserProfileBySSOID(this.ssoUser.data.user_id)
this.$store.commit('SET_USER', user)
}
So now I have the user's profile (user object) to use throughout the app. Works good....but...when a user edits his profile in the app (for example, updates his phone number, etc) and clicks submit, I can't seem to get the state to refresh/see that there has been a change unless the user manually refreshes the page.
What is the recommended way to handle this issue? Do I need to run a dispatch to the user state on every route change? The user's profile is located at path: '/userEdit/:uid'
This is my app structure:
<div id="app">
<Banner />
<section class="container-fluid">
<loading-spinner v-if="isBusy"></loading-spinner>
<div v-else>
<AuthName class="text-right" />
<MainNav />
<main id="routerView">
<transition name="component-fade" mode="out-in">
<RouterView :key="$route.fullPath" />
</transition>
</main>
</div>
</section>
</div>
User profile update function:
ApiService.putUserProfile(this.user)
.then(() => {
this.loading = false
this.$router.push('/admin/users')
}
})
.catch(err => {
if (err.response) {
this.errors = err.response.data
} else {
if (err.request) {
this.errors = err.request
} else {
this.errors = err.message
}
}
this.loading = false
console.error('Error from update', err)
})

after you update the detail of the user you can fire an event which may will fetch the data from the server behind the scene.
methods:{
loadData(){
await this.$store.dispatch('getSSOUser') ;
await this.$store.dispatch('fetchUserProfiles');
await this.setUser()
this.isBusy = false
},
updateInfo() {
this.form.put('api/profile').then((response) => {
let userData = response.data.userdata;
Fire.$emit('ProfileEvent');
}
},
},
created(){
this.loadData();
Fire.$on('ProfileEvent', () => {
this.loadData();
});
}
may be firing an event after the there is any changes saved in the profile page and executing the function that is called when the component is created after the fired event may resolve your problem. Hope you will get the idea from above code example.

Related

Problem with rendering data and heavy data loading

The first problem is that when getDetails(‘multiple’, ‘2’) function is called inside HTML, it takes some time until de data is displayed from v-for.
The second problem is that when I call the console.log(userDetails) from inside of anotherFunction() I got the undefined answer. It doesn’t wait for the this.getDetails(‘multiple’, ‘1’) to execute completely.
How can I improve the time for rendering, or should I use another way to display de data?
How can I make the second function to wait until the first function is complete?
VUE version: 2.7.10
<div id="app">
<p v-for="item in userDetails">item is displayed</p> //
<button #click="anotherFunction()">Click Me!</button>
</div>
<script>
export default {
name: 'App',
data: {
userDetails: []
}
}
// axios function
getDetails(actionType, idUser) {
axios.post("https://example.com/link", {
Username: username
}).then(response => {
const result = response.data;
// push data into variable
this.userDetails.push(result[0])
}).catch(error => {
this.showError('Error', 4000);
console.error('Error:' + error);
});
// another function from where I want to call the axios function
anotherFunction() {
this.getDetails('multiple', '1')
// call the userDetails into another function will output "undefined"
console.log(this.userDetails);
}

How to update a variable from an asynchronous function?

In my template, in a v-slot (which means users is not available in <script setup>), I have
<template v-slot:body-cell-assignedTo="props">
<q-td :props="props">
<div v-for="u in props.users" :key="u">{{u}}</div>
</q-td>
</template>
This displays
john
mary
I can enrich this information by calling an API:
fetch('https://example.com/api/user/john')
.then(r => r.json())
.then(r => console.log(r))
This displays in the console John de Brown, 1262-1423.
My question: how to combine these two mechanisms? In other words, how to asynchronously update the value in {{}}?
I would need to do something like
<div v-for="u in props.users" :key="u">{{enrichFetchFunction(u)}}</div>
but it would need to be asynchronous, and yet somehow return a value.
EDIT: I will ultimately enrich the source data that is displayed in the v-slot. I would still be interested, though, if waiting for such an asynchronous function there (à la await) is doable in Vuie.
I assume you are using Compositions API. See this playground
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
onMounted(async() => {
fetch('https://mocki.io/v1/67abcfb6-4f25-4513-b0f9-1eb6c4906413')
.then(r => r.json())
.then(r => users.value = r)
})
</script>
<template>
<div v-for="u in users" :key="u">{{u}}</div>
</template>
This is doable with Lifecycle hooks such as mounted(), yet you will need some sort of listener to react to the information being changed. here is an example that updates the values as soon as it is mounted and includes a button that will also update the values (you can run the code here in Vue SFC Playground):
<template>
<div id="app">
<h1 v-for="u in enrichedUsers" :key="u">{{ u }}</h1>
<button #click="myAsyncFunction">
update
</button>
</div>
</template>
<script>
// pseudo api
const fetchEnrichedAPI = function(user) {
return new Promise( (resolve, reject) => {
var enrichedUsers = []
if (user.includes('john')) {
enrichedUsers.push('John de Brown, 1262-1423')
}
if (user.includes('mary')){
enrichedUsers.push('Mary de Purple, 1423-1262')
}
setTimeout(() => {
resolve(enrichedUsers);
}, 300);
});
}
export default{
data() {
return {
props : { users: ['john','mary'] },
enrichedUsers: []
}
},
mounted() {
// when mounted run this async function
this.myAsyncFunction()
},
methods: {
async myAsyncFunction() {
// call api passing the list of users
await fetchEnrichedAPI(this.props.users)
.then((data) => {
// if api work
this.enrichedUsers = data;
return true;
})
.catch((e) => {
// if the api doesn't work
console.error(e);
this.enrichedUsers = this.props.users;
})
}
},
}
</script>
I am aware that this does not use props, but it does work. If you would like to expand this to use props you may be able to do this with computed properties or functions in the v-for. See this post for more info on that.

How to open vuetify dialog after user logs in to application

In my application I want to show a modal to introduce the user in my application, so it will appear only in the first time he logs in. What I am doing is storing isNewUser in the global state and using it to know if it should render the modal or not using the same process described in this answer. (I'm not using event bus)
Here is my parent component:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
When the user logs in and this component is rendered I saw the dialog being rendered and closing. If I hit f5 it reloads the page and dialog is showed correctly.
If I do the bellow modification it works, but I don't want to solve the problem this way since it won't work for all cases, it will depend on the speed of the user computer/internet.
mounted() {
setTimeout(() => {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
}, 2000);
},
I've tried using v-if as well
<template>
<Intro v-if="isNewUser" :value="true" #input="finishTutorial" />
</template>
<script>
export default {
components: {
Intro,
},
data() {
return {
isNewUser: false,
};
},
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
methods: {
async finishTutorial() {
this.$store.dispatch('auth/finishTutorial');
this.isNewUser = false;
},
},
};
</script>
You can use a computed property to do so:
computed: {
isNewUser() {
return this.$store.state.auth.user.isNewUser;
}
}
and in the template you would do like so:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>

Vuejs testing value after click

<!-- template -->
<div>
<textarea v-model="someText">{{someText}}</textarea>
<div v-if="hasError">Something wrong here</div>
<input v-on:click="store" type="submit" value="update" />
</div>
//script
{
data() {
hasError: false,
someText: ""
},
store(){
return axios.post('/my/api/endpoint', { myvalue: this.someText })
.then(() => {
this.hasError= false;
})
.catch(() => {
this.hasError= true;
};
}
}
//test
import { mount } from 'vue-test-utils';
import MyComponent from "./component.vue";
import * as httpMock from 'moxios';
import Vue from "vue";
it("notifies when updates fail", (done) => {
const wrapper = mount(MyComponent);
httpMock.stubFailure("PUT", "/my/api/endpoint",
{
status: 500
});
httpMock.wait(() => {
wrapper.find(".button").trigger ("click");
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
});
I have the above code to test error state in the vue app. Simply, i'm trying to test that if there is an error calling the server, a bit of text is displayed to say so. i've manually tested this in the browser and its all good, but i can't get a unit test around it. it just fails, saying expected '...' does not contain Something wrong here
probably something to do with the dom not being updated yet? But I thought that was what Vue.nextTick was for?
You're running wait before you actually trigger the axios call. Your call of the click event must be outside of wait.
wrapper.find(".button").trigger ("click");
httpMock.wait(() => {
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
})
Also, I'm assuming you're importing axios in the component, as I don't actually see the import.

Hide an item when is deleted with this.$http.delete()

I have website build with VueJS on frontend and I want to hide each item that is deleted.
I have in store.js a property eventIsActive set to true:
export const store = new Vuex.Store({
state: {
eventIsActive: true
}
})
In a ShowItems.vue(grid version) I have a the delete method where I set the eventIsActive to false:
removeEvent() {
this.$http.delete('/event/' + item)
.then((response) => {
this.$store.state.eventIsActive = false;
this.$router.push('/events');
})
.catch((error) => {
alertify.error('Error', error);
})
}
On the same page I have the computed method for this property eventIsActive:
computed: {
getActiveEvent() {
return this.$store.state.eventIsActive;
}
}
On the HTML for this page I use the method from computed to hide the deleted component.
<template>
<div class="col-6 col-lg-4" v-if="getActiveEvent">
<p>{{itemTitle}}</p>
<p>{{itemSubtitle}}</p>
</div>
</template>
The problem is that when I delete an item, the rest of items are hidden, not only the one that is deleted.
Before I just used a simple eventIsActive: true in data() and set it to false in the removeEvent(). Was easier to use it, but because I have ShowItems.vue(list version), too, if I delete an Item in grid version, in the list version will still be there, until I refresh the page.
Can someone help me in this direction?
Vuex state is a global state that is shared by all components in the app.
So when you change eventIsActive all elements get the same state (true/false) and act accordingly.
Since showing/hiding an item is related to that specific item state, you need to have a local state for each item and change only it.
so in the data attribute of the component, add an active flag and use it instead:
data () {
....
active: true
}
....
removeEvent() {
this.$http.delete('/event/' + item)
.then((response) => {
this.active = false;
this.$router.push('/events');
})
.catch((error) => {
alertify.error('Error', error);
})
}
....
<template>
<div class="col-6 col-lg-4" v-if="active">
<p>{{itemTitle}}</p>
<p>{{itemSubtitle}}</p>
</div>
</template>