How to update state in a component when the value changed at vuex store? - vue.js

In vuex store I have this mutations which receives msg from one component and is to show/hide prompt message at another component (Like You are logged in propmpt after successful login) :
setPromptMsg: function (state, msg) {
state.showPromptMsg = true;
state.promptMsg = msg;
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
sleep(3000).then(() => {
store.showPromptMsg = false;
state.promptMsg = '';
console.log('show message set to false');
});
},
In the compoenet, I receive showPromptMsg from the store as a computed property:
computed: {
showPromptMsg () {
return this.$store.state.showPromptMsg;
},
promptMsg () {
return this.$store.state.promptMsg;
}
}
The show/hide prompt message in the template:
<div v-show="showPromptMsg">
<div class="text-center" >
<strong> {{promptMsg}} </strong>
</div>
</div>
The problem is that when the prompt is timedout, i.e. showPromptMsg is set to false at the store, the change is not reflected into the component, so the notification box does not disappear.
I'm wondering what is the idiomatic way to resolve this problem?

The code is setting
store.showPromptMsg = false;
I expect you want
state.showPromptMsg = false;

In your NotificationBarComponent.vue template:
<div>
<div class="text-center" >
<strong> {{ message }} </strong>
</div>
</div>
In your NotificationBarComponent.vue component definition add a prop to pass custom message to display and on mounted start the timeout to hide the notification:
export.default {
props: ['message'],
mounted() {
window.setTimeout(() => {
this.$store.commit('handleMessageState', false)
}, 3000);
}
};
in your store create a property to manage the notification display isNotificationBarDisplayed: false
handleMessageState(state, payload) {
state.isNotificationBarDisplayed = payload;
}
anywhere you want to use it:
<notification-bar-component v-show="isNotificationBarDisplayed" message="Some message here"></notification-bar-component>
computed: {
isNotificationBarDisplayed () {
return this.$store.state.isNotificationBarDisplayed;
},
}

Related

Duplicate keys Vuex Getters every time route changes

I am getting an array of objects from firebase and showing them characters component in list format using V-for. Everytime I go to homepage and returning to characters page the list are getting multiplied and showing me duplicate keys.
characters.vue:
<template>
<ul class="characters-list">
<li v-for="allHero in getAllHeros" v-bind:key="allHero.id">
<router-link :to="{ name: 'characterDetail', params: { id: allHero.id } }">
<div class="hero-thumbnail">
<img :src="allHero.imageUrl" :alt="allHero.title" />
</div>
<div class="hero-detail">
<h3>{{ allHero.name }}</h3>
</div>
</router-link>
</li>
</ul>
</template>
import database from "#/firebase/init";
computed: {
...mapGetters({ getAllHeros: "getAllHeros" }),
},
created() {
database
.collection("heros")
.get()
.then(snapshot => {
snapshot.forEach(doc => {
let heros = doc.data();
heros.id = doc.id;
this.$store.dispatch('fetchAllHeros', heros)
});
});
}
VUEX Module -
const state = {
allHeros: []
};
const getters = {
getAllHeros: state => {
return state.allHeros;
}
};
const actions = {
async fetchAllHeros({ commit }, heros) {
commit("setAllHeros", heros);
}
};
const mutations = {
setAllHeros: (state, payload) => {
state.allHeros.push(payload);
}
};
When you route to a new page your Vuex store does not necessarily get reset to its initial state. Therefore every time that component is created you are adding more heros to the vuex store which is resulting in duplicate heros being added.
To prevent this, you can just use some simple logic to check if any heroes have been loaded:
created() {
if(this.getAllHeros.length == 0){
//Get heros from database..
};
}

Vue.js Send an index with #input event

Vue version : 3.1.1
Hey guys,
I'm working with dynamic Creation Component, which means a user can add whatever of component he wants.I create it base on this documentation dynamic component creation.
And I use this component vue image uploader.
I need to send an index when the user wants to upload the image, like this :
<div v-for="(line, index) in lines" v-bind:key="index">
{{index}}//if i log the index its 0,1,2,3 and its ok
...
<image-uploader
:preview="true"
:class-name="['fileinput', { 'fileinput--loaded': line.hasImage }]"
:capture="false"
:debug="0"
:auto-rotate="true"
output-format="blob"
accept="image/*"
#input="setImage(output , index)"
:ref="'fileUpload'+index"
>
...
And the setImage funciton :
setImage: function(output,index) {
console.log(index);
console.log(output);
return ;
this.lines[index].hasImage = true;
this.lines[index].image = output;
let formData = new FormData();
formData.append("file", output);
Ax.post(upload_route, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
// upload successful
})
.catch(error => console.log(error));
}
And the log result is:
The index always is 0 :(
How can i send an index when i want to upload it?
I read this passing event and index and test it but it's not working on component.
Because This is a custom event not a DOM event.
what should I do?
thanks.
Because you're actually passing the return value of setImage to the #input, not the method.
You can't just add extra parameters to setImage, as ImageUploader component just emit an image to the setImage. If you need to add extra parameters to that method, you need to create custom element that wrap ImageUploader.
It's something like this:
ImageUpload.vue
<template>
<image-uploader
:debug="0"
:autoRotate="true"
outputFormat="blob"
:preview="true"
:className="['fileinput', { 'fileinput--loaded' : hasImage }]"
:capture="false"
accept="image/*"
doNotResize="['gif', 'svg']"
#input="setImage"
v-on="listeners" />
</template>
<script>
export default {
props: {
index: {
required: true,
type: Number
}
},
data() {
return {
hasImage: false,
image: null
};
},
computed: {
listeners() {
const listeners = { ...this.$listeners };
const customs = ["input"];
customs.forEach(name => {
if (listeners.hasOwnProperty(name)) {
delete listeners[name];
}
});
return listeners;
}
},
methods: {
setImage(image) {
this.hasImage = true;
this.image = image;
this.$emit("input", this.index, image); // here, we emit two params, as index for the first argument, and the image at the second argument
}
}
};
</script>
Then, you can use that component something like this:
<template>
<div class="container">
<div v-for="(line, index) in lines" :key="index">
<image-upload :index="index" #input="setImage"/>
</div>
</div>
</template>
<script>
import ImageUpload from "./ImageUpload";
export default {
components: {
ImageUpload
},
data() {
return {
lines: ["1", "2", "3", "4"]
};
},
methods: {
setImage(index, image) {
console.log("Result", index, image);
}
}
};
</script>
See the working example: https://codesandbox.io/s/vue-template-ccn0e
Just use $event like this...
#input="setImage($event, index)"
...and you're done!

How does vuejs react to component data updated asynchronously

I am very new with vuejs and recently started to try to replace some old jquery code that I have and make it reactive with vuejs. The thing is I have a component that gets information from a nodejs server via socket.io asynchronously.
When I get the data and update my component's data I see the changes when I console log it but it does not change the DOM the way I want it to do.
What is the proper way to grab data asynchronously and use it inside a component? I post some parts of my code so you can see it. I will appreciate any advice you can give me. Thanks in advance!
Vue.component('chat', {
data() {
return {
chat: null,
commands: [],
chatOpened: false,
}
},
props: [
'io',
'messages',
'channels',
'connectChat',
'roomChat',
'user',
'userId',
'soundRoute',
],
methods: {
openChat() {
this.chatOpened = true;
},
closeChat() {
this.chatOpened = false;
},
},
created() {
this.chat = this.$io.connect(this.connectChat);
this.commands.push('clear');
let self = this;
$.each(this.channels, function(index, value) {
self.chat.emit('join', {room: index, user: self.user, userId: self.userId}, function(err, cb) {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
self.channels[index].loaded = true;
}
//some more code
}
});
});
console.log(this.channels);
},
template: `
<div>
<div id="container-chat-open-button" #click="openChat" :class="{hide : chatOpened}">
<div>+90</div>
<i class="fas fa-comment-alt"></i>
</div>
<div id="container-chat" class="chat__container" :class="{open : chatOpened}">
<div id="container-chat-close-button" #click="closeChat">
<span>
<div>
<i class="fas fa-comment-alt"></i>
#{{ messages.chat_lobby_icon_title }}
</div>
<i class="icon-arrowdown"></i>
</span>
</div>
<div id="alert-chat" class="chat__container-notifications animated flash"></div>
<div class="row">
<ul>
<li v-for="channel in channels" v-show="channel.loaded === true">Channel loaded</li>
</ul>
</div>
</div>
</div>
`
});
I would expect to see the list of channels with messsages but instead I don't see the list even thought I see my channels with the loaded attribute set to true (by default they all have this attribute set to false).
My guess is that it's this part that is not working as expected.
if (messages.length > 0) {
self.channels[index].loaded = true;
}
The reactive way of doing this is by setting the full object again.
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
EDIT 1:
this.channels.forEach((channel) => {
this.chat.emit('join', {room: index, user: self.user, userId: self.userId}, (err, cb) => {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
}
//some more code
}
});
})
You'll need to add support for the rest-spread-operator using babel.

Data of child component is passed on to it next sibling component on update

I have two components (RequestList and Request). Parent component lists the array of objects fetched from the server into its child component as props by calling the getRequestList() method.
From the child component an object can be deleted by calling the parent component's method deleteRequest(request_id) which was sent as props.
In the parent component method, it deletes the object by sending a delete request to the server. If it was succeful, then it calls the same method to fetch the lists of requests getRequestList().
The problem I am facing is that, in the child component, there is another method to vote the request VoteRequest(). If the voting was successful, it will update the vote status of a Request component, or if it fails, it will show an error.
Now suppose there is already an error, in one of the Request component (child) eg, first request object. And I delete that request, and the request list is updated. Then for some reason I don't know why, the error from the deleted component is still present on its next Request component. What am I doing wrong here?
Parent component (RequestList.vue):
template
<ul id="request_list" v-if="request_list_data.length > 0">
<li v-for="request in request_list_data">
<request :request_id="request.id"
:deleteRequest="deleteRequest"
:updateVotes="updateRequestVotes">
</request>
</li>
<p class="error" style="margin-top: 20px;" v-show="request_error !== ''">{{request_error}}</p>
</ul>
script
export default {
data: function () {
return {
request_list_data: [],
}
},
methods: {
getRequestList() {
const url = this.$store.state.website + '/api/activities/-requests/?ordering=-id';
this.$http.get(url)
.then(function (response) {
this.request_list_data = Object.assign([], response.data);
})
},
deleteRequest(request_id){
const url = this.$store.state.website + '/api/activities/-requests/' + request_id + '/';
this.$http.delete(url)
.then(function (response) {
this.getRequestList();
})
}
},
created: function () {
this.getRequestList();
}
}
Child component (Request.vue):
template
<div class="request">
<div class="vote-action" #click="VoteRequest('down')">▼</div>
</div>
<div class="_data">
<div class="request_error" v-show="vote_error !== ''">
<p class="error">{{vote_error}}</p>
</div>
<button v-show="selfrequest" class="deleterequest_btn" #click="deleterequest()">Delete your request</button>
</div>
</div>
script
export default {
name: 'request_wrapper',
props: {
request_id: {
type: Number,
},
updateVotes: {
type: Function
},
deleteRequest: {
type: Function
}
},
data: function () {
return {
vote_error: ''
}
},
methods: {
VoteRequest(vote_type) {
this.vote_error = '';
if (this.$store.state.user_logged_in) {
if (vote_type === 'up') {
const data = {
'request': this.request_id,
'vote_type': true
};
this.PostVote(data);
}
else if (vote_type === 'down') {
const data = {
'request': this.request_id,
'vote_type': false
};
this.PostVote(data);
}
},
PostVote(data) {
const url = this.$store.state.website + '/api/activities/-request-votes/none/voterequest/';
this.$http.post(url, data)
.then(function (response) {
console.log(response.data);
this.updateVotes(this.request_id, response.data.votes);
})
},
deleterequest() {
this.deleteRequest(this.request_id);
}
}
}
This is always a good practice to add unique key to elements inside v-for loop.
Using unique keys allows to avoid weird behavior when you delete items from an array rendered with v-for. It helps Vue decide weither to render new data or show the cached one: https://v2.vuejs.org/v2/guide/list.html#key
Starting from Vue 2.2 using key is required when looping components: https://v2.vuejs.org/v2/guide/list.html#Components-and-v-for

VueJS: State from deleted component remains and affects the next one (sibling)

I have a notifications component that has some notifications item child components that are fed from an array in the parent component. The child component has the ability to update and delete itself. It can mark itself as read when clicked. It will make a request to the server with Axios and then change a button icon to close (fa-close). Which works fine. Now it can delete itself. When clicked it will send a delete request to the server, and when successful emit an event to the parent component to delete it from the array with splice. Now it works fine but the issue I'm having is that the new icon that I changed still remains for the next component (next item in the array). And that bugs me because I can't seem to find a way to make it display the initial icon which was initialize with the component. here's some code if that can help NotificationsItem.vue <template>
<li class="list-group-item list-group-item-info">
<button class="pull-right"
title="#lang('text.notifications.markAsRead')"
#click="markAsReadOrDestroy">
<i class="fa" :class="iconClass" v-show="!loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" v-show="loading"></i>
</button>
<!-- {{ notification.data }} -->
I'm the index {{ index}} and the ID is {{notification.id}}
<span class="hljs-tag"></<span class="hljs-name">li</span>></span>
</template>
<script>
export default {
props: ['notification', 'index'],
data() {
return {
loading: false,
icon: 'check',
markedAsRead: false,
}
},
computed: {
iconClass() {
return 'fa-' + this.icon;
}
},
methods: {
markAsReadOrDestroy() {
if (this.markedAsRead) {
this.destroy();
} else {
this.markAsRead();
}
},
markAsRead() {
let vm = this;
this.loading = true;
this.$http.patch('/notifications/markasread/' + this.notification.id)
.then(function(response) {
console.log(response);
vm.loading = false
vm.markedAsRead = true
vm.icon = 'close'
})
.catch(function(error) {
console.log(error);
vm.loading = false;
});
},
destroy() {
let vm = this;
this.loading = true;
this.$http.delete('/notifications/' + this.notification.id)
.then(function(response) {
console.log(response);
vm.loading = false;
vm.$emit('deleted', vm.index);
})
.catch(function(error) {
console.log(error);
vm.loading = false;
});
}
},
mounted() {
console.log('Notifications Item mounted.')
}
}
</script>
NotificationsList.vue <template>
<div class="list-group">
<notifications-item
v-for="(notification, index) in notifications"
:notification="notification"
#deleted="remove"
:index="index">
{{ notification.data['text'] }}
</notifications-item>
</div>
</template>
<script>
export default {
data() {
return {
notifications: notifications.data,
}
},
methods: {
remove(index) {
console.log(index);
this.notifications.splice(index, 1);
}
},
mounted() {
console.log('Notifications List mounted.')
}
}
</script>
If anyone can help me that would be greatly appreciated.
You need to pass index as paramter in remove function, like following:
<notifications-item
v-for="(notification, index) in notifications"
:notification="notification"
#deleted="remove(index)"
:index="index">
{{ notification.data['text'] }}
</notifications-item>
I found a fix by adding a key attribute on the child component with a unique value (the notification id). And that's it.