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

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.

Related

Vue - Render new component based on page count

I'm working on an onboarding process that will collect a users name, location, job , etc.
It needs to be one question per page but as an SPA so I currently have around 20 components to conditionally render.
Atm, I have a counter and Prev/Next buttons that decrease/increase the counter respectively. I'm then using v-if to check what number the counter is on and render the appropriate page.
Is there a better way around this that is less repetitive and bulky?
Any ideas appreciated!
data() {
return {
onboardingStep: 0,
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
<intro-step v-if="onboardingStep === 0"></intro-step>
<first-name v-if="onboardingStep === 1"></first-name>
<last-name v-if="onboardingStep === 2"></last-name>
...etc.
Suggestion :
You can make your field components to show or hide based on the prev/next state. Dynamic components provide that platform in an efficient and simple way.
Syntax :
<component :is="componentName"></component>
Then, You can create each component instance dynamically by putting a watcher on components array.
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
template: `template code will come here`
})
});
}
}
}
Live Demo :
new Vue({
el: '#app',
data() {
return {
components: [],
onboardingStep: 0
}
},
mounted() {
this.components = ['intro-step', 'first-name', 'last-name'];
},
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
data() {
return {
modelName: cName
}
},
template: '<input type="text" v-model="modelName"/>'
})
});
}
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(cName, index) in components" :key="index">
<component :is="cName" v-if="index === onboardingStep"></component>
</div>
<button #click="prevStep" :disabled="onboardingStep < 1">Prev</button>
<button #click="nextStep" :disabled="onboardingStep === components.length - 1">Next</button>
</div>
You could create an array with all your component names in the right order.
const components = ['intro-step', 'first-name', 'last-name' ]
And then with a v-for loop set all the components in your template:
<template v-for="(component, index) in components" :key="component">
<component :is="component" v-if="index === onboardingStep">
</template>
Hope this helps.

Vue.js prop sync modifier not updating parent component

I have a property that I need to pass to a child component, but the child component needs to be able to modify the value that was passed. It seems like the .sync modifier is built for this, but I can't seem to get it to work. Here is my code (simplified for this question):
Profile.vue
<template>
<div>
<Avatar :editing.sync="editing"></Avatar>
click to change
...
</div>
</template>
<script>
import Avatar from './profile/Avatar'
export default {
components: { Avatar },
data() {
return {
...,
editing: false,
}
},
methods: {
editAvatar() {
this.editing = true;
}
}
}
</script>
Avatar.vue
<template>
<div>
<template v-if="!editing">
<img class="avatar-preview" :src="avatar">
</template>
<template v-else>
<label v-for="image in avatars">
<img class="avatar-thumb" :src="image">
<input type="radio" name="avatar" :checked="avatar === image">
</label>
<button class="btn btn-primary">Save</button>
</template>
</div>
</template>
<script>
export default {
props: ['editing'],
data() {
return {
avatar: '../../images/three.jpg',
avatars: [
'../../images/avatars/one.jpg',
'../../images/avatars/two.jpg',
'../../images/avatars/three.jpg',
...
]
}
},
methods: {
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.editing = false; }); // ← this triggers a Vue warning
}
}
}
</script>
You are correct - the .sync modifier is built for cases just like this. However, you are not quite using it correctly. Rather than directly modifying the prop that was passed, you instead need to emit an event, and allow the parent component to make the change.
You can resolve this issue by changing the save() method in Avatar.vue like this:
...
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.$emit('update:editing', false); });
}
}
...

How to update state in a component when the value changed at vuex store?

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;
},
}

Vue.js data: undefined

I am new to Vue.js.
Please advice me.
I get comments: undefined so comments are not displaying.
xhr is normal with 200.
Thank you
Thank you
Thank you
Thank you
Thank you
<template>
<div>
<ul class="media-list">
<li class="media" v-for="comment in comments">
{{ $comment.body }}
</li>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
comments: []
}
},
props: {
postid: null
},
methods: {
getComments () {
this.$http.get('/blog/' + this.postid + '/comments').then((response) => {
this.comments = response.json().data;
});
}
},
mounted () {
this.getComments();
}
}
Basically there are two problems:
$comment don't exist
You have no data on response.json().data, that's why you get a undefined
I used a different API just to test it (as I don't have access to yours).
TEMPLATE
<div id="app">
<ul class="media-list">
<li class="media" v-for="comment in comments">
{{ comment.familyName + ', ' + comment.givenName }}
</li>
</ul>
</div>
SCRIPT
new Vue({
el: '#app',
data () {
return {
comments: []
}
},
props: {
postid: null
},
methods: {
getComments () {
this.$http.get('//ergast.com/api/f1/drivers.json').then((response) => {
this.comments = response.body.MRData.DriverTable.Drivers;
});
}
},
mounted () {
this.getComments();
}
});
Check out a working example here
this.comments = response.json().data;
console.log(this.comments) ;
to see what you get ;
you define comments=Array ;
maybe you get the response.json().data is not a Array;
Try using vm instead of this. In API response make sure what you are getting using console.log(). If response is already in json do not use response.json(). In HTML change $comment.body to comment.body. Make sure you have the body key in comments[] array.
<template>
<div>
<ul class="media-list">
<li class="media" v-for="comment in comments">
{{ comment.body }}
</li>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
comments: [],
postid: null
}
},
props: {
},
methods: {
getComments () {
let vm = this;
vm.$http.get('/blog/' + vm.postid +
'/comments').then((response) => {
console.log(response)
vm.comments = response.data;
});
}
},
mounted () {
let vm = this;
vm.getComments();
}
}
}
:
My suggestion is to properly use try-catch statements.
I have found this is the safest and proper way to manage cases where variable could take undefined or null values, instead of trying to "if" everything.
try {
val = localStorage.getItem('accesstoken')
} catch (error) {
alert(error)
}
Take care!

Vuejs reactive v-if template component

I'm struggling to understand how to make my component reactive. At the moment the button is rendered correctly but once the create/delete event happens, the template does not change. Any tips on how to update the component after the event has taken place?
new Vue({
el: '#app'
});
Vue.component('favourite-button', {
props: ['id', 'favourites'],
template: '<input class="hidden" type="input" name="_method" value="{{ id }}" v-model="form.listings_id"></input><button v-if="isFavourite == true" class="favourited" #click="delete(favourite)" :disabled="form.busy"><i class="fa fa-heart" aria-hidden="true"></i><button class="not-favourited" v-else #click="create(favourite)" :disabled="form.busy"><i class="fa fa-heart" aria-hidden="true"></i></button><pre>{{ isFavourite == true }}</pre>',
data: function() {
return {
form: new SparkForm({
listings_id: ''
}),
};
},
created() {
this.getFavourites();
},
computed: {
isFavourite: function() {
for (var i = 0; this.favourites.length; i++)
{
if (this.favourites[i].listings_id == this.id) {
return true;
}
}
},
},
methods: {
getFavourites() {
this.$http.get('/api/favourites')
.then(response => {
this.favourites = response.data;
});
},
create() {
Spark.post('/api/favourite', this.form)
.then(favourite => {
this.favourite.push(favourite);
this.form.id = '';
});
},
delete(favourite) {
this.$http.delete('/api/favourite/' + this.id);
this.form.id = '';
}
}
});
Vue.component('listings', {
template: '#listing-template',
data: function() {
return {
listings: [], favourites: [],
};
},
created() {
this.getListings();
},
methods: {
getListings() {
this.$http.get('/api/listings')
.then(response => {
this.listings = response.data;
});
}
}
});
Vue expects HTML template markups to be perfect. Otherwise you will run into multiple issues.
I just inspected your template and found an issue - the first <button> element does not close.
Here is the updated version of your code:
Vue.component('favourite-button', {
props: ['id', 'favourites'],
template: `
<input class="hidden" type="input" name="_method" value="{{ id }}" v-model="form.listings_id"></input>
<button v-if="isFavourite == true" class="favourited" #click="delete(favourite)" :disabled="form.busy">
<i class="fa fa-heart" aria-hidden="true"></i>
</button> <!-- This is missing in your version -->
<button class="not-favourited" v-else #click="create(favourite)" :disabled="form.busy">
<i class="fa fa-heart" aria-hidden="true"></i>
</button>
<pre>{{ isFavourite == true }}</pre>
`,
...
Note the comment on 7th line above, the closing </button> tag is not present in your template.
As a side note, if you do not want to type back-slash at the end of every line to make multi-line template strings, you can use back-ticks as shown in my code example above. This will help you avoid markup errors leading to Vue component issues and many hours of debugging.
Another reference: Check out "Multi-line Strings" in this page: https://developers.google.com/web/updates/2015/01/ES6-Template-Strings
Relevant lines (copied from above page):
Any whitespace inside of the backtick syntax will also be considered part of the string.
console.log(`string text line 1
string text line 2`);
EDIT: Found a possible bug in code
Here is another issue in your create method of favourite-button component:
methods: {
// ...
create() {
Spark.post('/api/favourite', this.form)
.then(favourite => {
this.favourite.push(favourite); // Note: This is the problem area
this.form.id = '';
});
},
//...
}
Your success handler refers to this.favourite.push(...). You do not have this.favourite in data or props of your component. Shouldn't it be this.favourites?