Vue2 error when trying to splice last element of object - javascript

I have a Vue2 app wit a list of items which I can choose and show, or delete.
When deleting the last element in the list (and only the last one) - I get Vue warn - "[Vue warn]: Error when rendering root instance: "
my HTML:
<body >
<div id="app">
<ul>
<li v-for="(item, index) in list" v-on:click = "selectItem(index)" >
<a>{{ item.name }}</a>
<div v-on:click="deleteItem(index)">X</div>
</li>
</ul>
<div>
<span>{{selectedItem.name}}</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
The JS:
var app = new Vue({
el: '#app',
data: {
index: 0,
selectedItem: {},
list : [
{ id: 1, name: 'org1', desc: "description1"},
{ id: 2, name: 'org2', desc: "description2"},
{ id: 3, name: 'org3', desc: "description3"},
{ id: 4, name: 'org4', desc: "description4"}
]
},
methods: {
deleteItem: function(index) {
this.list.splice(index,1);
},
selectItem: function(index) {
this.selectedItem = this.list[index];
},
}
})
Can you please advise why does this happen and how to solve this issue?

The problem is happening as you have having selectItem bind at li level, so event when you click cross button, selectItem gets executed and that same item gets deleted as well, causing this error.
One way to solve this problem can be moving the selectItem binding inside li as follows
<li v-for="(item, index) in list">
<a v-on:click = "selectItem(index)" >{{ item.name }}</a>
<div v-on:click="deleteItem(index)">X</div>
</li>
See working fiddle.
Another approach can be when printing selectedItem.name in your HTML, you put a null check, whether selectedItem exist or not like following:
<span>{{selectedItem && selectedItem.name}}</span>
See Working fiddle.

Related

Remove duplicate list item in v-for

I'm retrieving all groups from an axios request at the page load and I store that data inside an empty array inside a reactive object like this,
const groupHandler = reactive({
groups: [],
});
And when the user clicked one of his product and click on edit a form will appear like this,
Here you can see Group 1 has been repeated. And there is another reactive object to store that user's product's group id,
const productForm = reactive({
group: 2,
});
So when the user clicks on a product productForm.group will be filled with that product's group id. I want to prevent this been duplicated in my edit product form. I'm using the v-for directive to loop the groups array,
<li
v-for="group in groupHandler.groups"
:key="group.id"
:group-id="group.id" >
{{ group.name }}
</li>
So how to prevent this duplicate? In the v-for directive, I could use a condition like if group.id is not equal to productForm.group print group.name But I have no clue to do this. Really appreciate if somebody could help thanks.
You can use v-for in the template element, and then in the li element, you can use v-if condition to only render the group which doesn't have that id
<template v-for="group in groupHandler.groups">
<li
v-if="group.id !== productForm.group"
:key="group.id"
:group-id="group.id" >
{{ group.name }}
</li>
</template>
You can simply achieve this by using Array.filter() method in the v-for directive itself.
v-for="group in groupHandler.groups.filter(({ name }) => !uniqPropValue[name] && (uniqPropValue[name] = true))"
Live Demo :
new Vue({
el: '#app',
data: {
uniqPropValue: {},
groupHandler: {
groups: [{
id: 1,
name: 'Group 1'
}, {
id: 2,
name: 'Group 1'
}, {
id: 3,
name: 'Group 2'
}, {
id: 4,
name: 'Group 3'
}]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<li
v-for="group in groupHandler.groups.filter(({ name }) => !uniqPropValue[name] && (uniqPropValue[name] = true))"
:key="group.id"
:group-id="group.id">
{{ group.name }}
</li>
</div>

v-model on input change is heavy on performance

So I have a page rendering a v-list based on an array like so :
<v-list-tile v-for="item in array">
{{item}}
</v-list-tile>
and a dialog with a v-text-field :
<v-dialog>
<v-text-field v-model="myInput">
</v-text-field>
</v-dialog>
For now it's pretty normal.
But with a performance test, I saw that for every event triggered by a change on myInput model (like a key press) the v-for is also triggered re-rendering the list when they are actually not related.
On my huge array, it's a serious problem and make the UI really laggy. I think it's a normal behavior for a vuejs application, but I was wondering if I could precisely tell wish element to check for re-rendering.
I tried some v-if statements but it didn't do the trick.
I hope that there is an answer to that, i guess i'm missing something.
If you want to test what i'm talking about here is a ready to go html file, please debug it with your debug console, you will see a [vue warn] message of the duplicated key attesting of the fact that the v-for is indeed called for every key press.
Imagine now if the array (here items) is way bigger than that, and wrapped into complex components, making that call is just too heavy on performance when we are just aiming to change the "myInput" value.
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{data}}
<ul>
<li v-for="item in items" :key="item">
{{ item.message }}
</li>
</ul>
<input v-model="data"></input>
</div>
</body>
<script>
new Vue({
el: '#app',
data: () => ({
data: '',
items: [{
message: 'Foo'
},
{
message: 'Bar'
}
]
})
})
</script>
</html>
Here's a codepen showing the inner loop in its own component
Codepen.io
I've added Date.now() after items[x].message list items to show when the list is being rerendered.
In case codepen ever goes down:
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
Main vue: {{data}}
<loop-component :data="loopdata"></loop-component>
<input v-model="data"></input>
<input v-model="loopdata"></input>
</div>
<script>
Vue.component('loop-component', {
props: ['data'],
data() {
return {
items: [
{message: 'Foo'},
{message: 'Bar'}
]
}
},
template: `
<div>
Loop component: {{ data }}
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.message + ' Date.now(): ' + Date.now() }}
</li>
</ul>
</div>
`
});
let app = new Vue({
el: '#app',
data: () => ({
data: '',
'loopdata': '',
items: [
{message: 'Foo'},
{message: 'Bar'},
]
}),
});
</script>
</body>
</html>
Try using .lazy modifier to sync after change events.
<input v-model.lazy="data"></input>
https://v2.vuejs.org/v2/guide/forms.html#lazy
EDIT
#IVO GELOV is right, when a component changes, this re-render. The solution is split your component into several child components.
https://v2.vuejs.org/v2/guide/reactivity.html
This is a code using slots to make it look like your example.
HTML
<div id="app">
<new-component>
<ul>
<li v-for="item in items" :key="item">
{{ item.message }}
</li>
</ul>
</new-component>
</div>
Javascript
Vue.component('new-component', {
data: () => {
return {
data: ''
}
},
template: `
<div>
<div>{{ data }}</div>
<slot></slot>
<input v-model="data"></input>
</div>`
})
new Vue({
el: '#app',
data: () => ({
items: [{
message: 'Foo'
},
{
message: 'Bar'
}
]
})
})
Since Vue 2.0+ whenever a change is detected - the whole component is re-rendered. If you want to avoid that - split your component into several child components.
Your example does not prove your point - the fact that there is a warning about duplicate keys inside the v-for does not mean that v-for is re-evaluated on each keypress. To confirm my statement - just change your code like this:
<li v-for="(item,idx) in items" :key="idx">
Now there is no warning.

How render component in v-for by button from parent

How i can render v-if component by button(button in parent) click inside v-for loop? and should render only in that item where clicked
<div v-for="item in items">
<button #click >Show child<button>
<div>{{item.name}}</div>
<child v-if="this button clicked" :item="item"><child>
<div>
You have to store info about state of every item (if it was clicked) in your data. Then, when you click on button you should update clicked property for particular item. Finally if item.clicked is set on true you will show your child component (or any other html).
<template>
<div>
<div v-for="item in items" :key="item.id">
<button #click="item.clicked = true" >Show child</button>
{{item.name}}
<div v-if="item.clicked">Item child</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: function() {
return {
items: [
{
id: 1,
name: 'test1',
clicked: false
},
{
id: 2,
name: 'test2',
clicked: false
},
{
id: 3,
name: 'test3',
clicked: false
}
]
}
}
}
</script>
Plain and simple you just have to set some flag for latter v-if:
<div id="app">
<div v-for="item in items">
<button #click="$set(item, 'shown', true)">Show child</button>
<div>{{ item.name }}</div>
<div v-if="item.shown">Child component</div>
</div>
</div>
Here, $set() is used because initial item could lack shown field, so setting it directly with item.shown=true won't be reactive.
You can also hide button after click:
<button #click="$set(item, 'shown', true)" v-if="!item.shown">Show child</button>
To toggle visibility you just have to do it like this:
<button #click="$set(item, 'shown', !item.shown)">
{{ item.shown ? 'Hide' : 'Show' }} child
</button>
JSFiddle
You can take advantage of an item... index available in v-for directive (e.g. v-for="(item, i) in items"), to bind it (index of the item) to the function which shows an item by changing it's property:
Update: Initial answer has been deleted after requirements refinement.
Since you prefer to keep from mutation of items, you can wrap them in Map object (as keys) and keep visibility settings separately as Map values. Unfortunately, as far as I know, for the time being Vue.js does not support reactivity for Map objects, that's why I have to trigger rerendering manually by using forceUpdate:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('child', {
template: '<p>Visible child</p>'
})
new Vue({
el: "#demo",
template: `
<div>
<div v-for="item in items">
<button #click="toggleChild(item)">Toggle child</button>
<div>{{item.name}}</div>
<child v-if="isVisible(item)" :item="item"></child>
</div>
</div>
`,
data () {
return {
itemsMap: new Map(
[
{ name: 'test1' },
{ name: 'test2' }
].map(item => [item, { visible: false }])
)
};
},
methods: {
toggleChild(item) {
this.itemsMap.set(item, { visible: !this.itemsMap.get(item).visible });
this.$forceUpdate();
},
isVisible(item) {
return this.itemsMap.get(item).visible;
}
},
computed: {
items: function() {
return Array.from(this.itemsMap.keys());
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo"></div>

Vue JS toggle class on individual items rendered with v-for

I am using the v-for directive to render a list .
<li v-for="group in groupList" :key="group.id" #dragenter="toggleClass ...."#dragleave="toggleClass ...." >
Content
</li>
What I want is to add a class to the li on which the dragenter event is fired ?
How can I accomplish this ?
How do I even get a reference to the item (the item,not the data property of the parent component)in the first place inside the event handle?and even If I get the reference how to toggle the class from there?
Thanks.
I know vue is data-driven , change the data to reflect on the DOM but I would like a concise solution to this rather than index/Id on the data-model based solutions.Thanks
You can access the li being dragged in the dragenter-callback by accessing event.currentTarget (or even event.target would work in this case), where event is the callback's parameter.
new Vue({
el: '#app',
data() {
return {
grouplist: [
{ id: 1, text: 'a' },
{ id: 2, text: 'b' },
{ id: 3, text: 'c' },
]
}
},
methods: {
onDragEnter(e) {
e.currentTarget.classList.add('drag-enter');
},
onDragLeave(e) {
e.currentTarget.classList.remove('drag-enter');
}
}
})
.drag-enter {
background: #eee;
}
<script src="https://unpkg.com/vue#2.5.16"></script>
<div id="app">
<p draggable>Drag this text over the list items below</p>
<ul>
<li v-for="group in grouplist"
:key="group.id"
#dragenter="onDragEnter"
#dragleave="onDragLeave">{{group.text}}</li>
</ul>
</div>

binding the property of a data object to DOM element's attribute

I'm a newbie in Vue.js. I have the following lines of code in my HTML and JS file:
HTML
<div id="app">
<ul>
<li v-for="item in items" v-bind:class="{{item.className}}">{{item.text}}</li>
</ul>
</div>
JS
var app = new Vue({
el: '#app',
data: {
items: [
{
className: 'item-1',
text: 'Item 1'
},
{
className: 'item-2',
text: 'Item 2'
},
{
className: 'item-3',
text: 'Item 3'
}
]
}
})
What I want to happen is bind the value of each className to the class attribute of each DOM element. I hope someone could correct me on this.
When using v-bind you don't need to use the {{...}} syntax, because Vue already assumes you will want to use some kind of a property or object.
So you can for example output the value of each className simply like this:
<li v-for="item in items" v-bind:class="item.className">{{item.text}}</li>
Or shorthand version:
<li v-for="item in items" :class="item.className">{{item.text}}</li>
Or if the classes are always going to follow the pattern of item-i:
<li v-for="item, i in items" :class="`item-` + i">{{item.text}}</li>

Categories

Resources