VueJS v-for unwanted behaviour - vue.js

I get this problem whenever I modify an array that is used to render a v-for list.
Let's say I've got a v-for list of three items:
<ul>
<li v-for="item in items"></li>
<ul></ul>
<ul>
<li>One</li> <!-- Has focus or a specific child component -->
<li>Two</li>
<li>Three</li>
</ul>
Add a new item to the items array:
<ul>
<li>New Item</li> <!-- Focuses on this item, the child component seems to be moved here -->
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
The focus seems to move...
Please have a look at a fiddle that illustrates the problem https://jsfiddle.net/gu9wyctr/
I understand that there must be a good reason for this behaviour, but I need to manage it or avoid completely. Ideas?
EDIT:
I've just realized that my explanation is rather ambiguous. Here's an updated fiddle to illustrate the problem https://jsfiddle.net/keligijus/d1s4mjj7/
The problem is that the input text is moved to another element...
My real life example. I've got a forum-like list of posts. Each post has an input for a reply. If someone publishes a new post while other user is typing in a reply, the input that this user is typing in is moved to another post. Just like the example in the fiddle.

Providing key is the answer!
https://v2.vuejs.org/v2/guide/list.html#key
When Vue is updating a list of elements rendered with v-for, it by default uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will simply patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values (using shorthand here):
<li v-for="(item, index) in items" :key="'item-'+item">
<input :id="'item-'+index" type="text" style="width:80%;">
</li>
Updated fiddle to show that this works https://jsfiddle.net/keligijus/d1s4mjj7/3/

Try this:
var app = new Vue({
el: '#app',
data: {
messages: [
{ message: 'Hello Vue!', id: 0 },
{ message: 'Hello Vuex!', id: 1 },
{ message: 'Hello VueRouter!', id: 2 }
],
msg: null,
focus: 'item-1'
},
mounted () {
document.getElementById(this.focus).focus()
setTimeout(() => {
this.messages.unshift({ message: 'Focus moves!', id: 3 })
}, 2000)
setTimeout(() => {
this.messages.unshift({ message: 'Moves again...', id: 4 })
this.msg = `I suppose this happens because of the way DOM is updated and I understand there must a good reason for this. However I need to avoid this behaviour. How can I do this?`
}, 4000)
},
updated: function () {
document.getElementById(this.focus).focus()
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="(message, index) in messages">
<input :id="'item-'+message.id" type="text" v-model="message.message" style="width:80%;">
</li>
<li v-if="msg">{{msg}}</li>
</ul>
</div>
Basically I make id the same even when new items are added, and then I can track the focused item, and focus them again even after updated.

Related

How to display data from Vuex store in life time in Vue

I want o show data from Vues store. So first I want to check if authentication true, if it is, I want to show data from Vuex. Here is my shortcode:
<li v-if="authenticated">
Hello, {{ getUser.attributes.first_name }}
</li>
computed: {
getUser() {
console.log(this.$store.state.user)
return this.$store.state.user;
}
},
But I am getting error like you see in the picture below, why do you think it might be? Why first the object is coming empty and then object is filled?
Add a condition to the v-if directive because at the first rendering the attributes property is not available :
<li v-if="authenticated && getUser.attributes">
Hello, {{ getUser.attributes.first_name }}
</li>

VueJS making API calls for every item in v-for and returning them to the right position

Thank you in advance.
So I am fetching list of blog categories via API and rendering it in a list using v-for.
I also need to fetch the amount of blogs in every category and place them beside the category.
But the issue is I am calling a method that calls the api.
<li v-for="item in sidebar" :key="item.identifier">
<nuxt-link
tag="a"
:to="{
name: 'blog-page',
query: { category: item.identifier }
}"
>{{ $localize(item.translations).title }}
{{ getBlogCount(item.identifier) }}
</nuxt-link>
</li>
You know what it shows already example is Animals [Object Promise]
methods: {
async getBlogCount(identifier) {
axios
.get(
"https://example.com/posts?fields=created_at&filter[category.category_id.identifier]=" +
identifier +
"&meta=*"
)
.then(count => {
return count.data.meta.result_count;
});
}
}
What is the best way to handle this kinda thing?
You better call async methods in mounted or created hooks, and set the result to data, and then, use that data in template.
I'd suggest handling this in Script, instead of HTML Template.
What you can do is, depending on when the sidebar is initialized (maybe in the mounted hook), call getBlogCount method to fetch blog counts for each item in sidebar and store that may be in an array or object (or as a separate key-value pair to that same sidebar item object) and then use that data structure to display count values in the template.
Assuming the sidebar is populated in mounted hook and that it's an array of objects, you can do the following:
<template>
<li v-for="item in sidebar" :key="item.identifier">
<nuxt-link
tag="a"
:to="{
name: 'blog-page',
query: { category: item.identifier }
}"
>{{ $localize(item.translations).title }}
{{ item.blogCount }}
</nuxt-link>
</li>
</template>
<script>
mounted () {
// after the sidebar is populated
this.sidebar = this.sidebar.map(async item => {
item.blogCount = await this.getBlogCount(item.identifier)
return item
})
}
</script>
Hope this helps you out

How to reset value of Child componenet when filter is used in Vue2

I have this fiddle where if you use the inputSpinner and then use filter at the top the value of the input spiner stays the same
JsFiddle
My problem is like this
Code
<script type="text/x-template" id="grid-template">
<div class="container" style="margin-top:10px;">
<ul class="list-group">
<li v-for="entry in filteredData" class="list-group-item" >{{entry.name}} <div><numberinputspinner
:min="0"
:max="2"
:step="0.0001"
:card="entry"
#newNumber="updateTable"
/></div></li>
</ul>
</div>
</script> ....
When you filter the data, Vue destroys/recreate the unused components (for example if looking for Bruce Lee, would destroy Chuck Norris), as components get destroyed you will lose the data, as is not persistent.
You need to keep that data sync with the parent so when recreated again it would reassign its previous value.
Here's the updated jsFiddle: https://jsfiddle.net/myeu0sL3/9/
What I just did was pass the data to the parent in the newNumber event, and assign it to the collection, like this:
updateTable:function(card, data){
card.value = data;
}
and then in the component just assign it whenever is passed:
data: function () {
const vm = this;
return {
numericValue: vm.card.value,
};
},
and emit the card in the newNumber event:
this.$emit('newNumber', this.card, parseFloat(val).toFixed(4), parseFloat(oldVal).toFixed(4) );
Oh and finally there was a mistake in your props declaration, you had it twice, so I merged to include the cards:
card: {
type: Object,
required: true
},
That's it :)

how to select first radio button without knowing the value using vue.js (and v-model)

In my use case the radio buttons is initially dynamically rendered by server. Then in client side I want to use v-model to handle the value change and related logic. The problem is that I want to pre-select the first button but in client side I don't know the value of it. What is the recommended way to do this in vue.js? If necessary the server can generate some "hint" for it.
I've been searching for a while but every working solution I saw assume the data is already there. One method I can think of is to make an api to return the buttons' value but this feels overkill.
JSFiddle
You can set the initial value of r in the mounted hook of the component to the value of the first radio button:
mounted() {
this.r = this.$el.querySelector('input[type="radio"]').value;
}
But it might be better to use a fixed template for the component that isn't rendered by the server in the first place. Instead of rendering HTML, you could render JavaScript containing the data for the items you want and then bind that data to the template.
Vue works best when the template is generated from the data instead of the other way around.
<script>
// Rendered by the server external to the Vue component,
// otherwise just render directly into the component's data
window.items = [
{ label: 'Apple', value: 'apple' },
{ label: 'Orange', value: 'orange' },
];
</script>
<label>
<input v-model="selected" v-for="item of items" type="radio" :value="item.value">
{{ item.label }}
</label>
new Vue({
el: "#app",
data: {
items: window.items,
selected: window.items[0].value,
},
});

What's the point of using track-by attribute along with v-for?

I'm a newbie at vuejs. It seems to me the track-by attribute is a little hard to understand. The following example shows the track-by attribute has something to do with the duplicate elements in an array.But how is the v-for attribute implemented under the hood? What's the behavior when there are duplicate elements in an array and how track-by="$index" comes to make a difference?
new Vue({
el: '#app',
data: function() {
return {
items: [
'User Connected',
'Message',
'Message',
'User Connected',
'Message'
]
}
},
methods: {
addItem: function(item) {
this.items.push(item);
}
}
})
<div id="app">
<button #click="addItem('User Connected')">Add Connected</button>
<button #click="addItem('Message')">Add Message</button>
<ul>
<li v-for="item in items" track-by="$index">{{ item }}</li>
</ul>
<pre>
{{items | json}}
</pre>
</div>
https://jsfiddle.net/uuw4z0kr/2/
In order to be reactive and fast, Vue re-uses DOM elements whenever possible. So if it has already rendered the DOM for a particular item, it will save it to use anytime that needs to be rendered again. If something is removed from the array, then added back in, it will be faster to use existing HTML.
But this causes issues when the array elements are not unique. Vue can't distinguish them. track-by tells Vue which aspect of each item is unique, so it can know when to re-use DOM elements. If your array is a series of objects with an id attribute, you can use track-by='id'. But if the objects don't have a unique field, track-by='$index' associates each object with its position in the array. This is inherently a unique attribute, so it suppresses the error for duplicate entries.