Vue.js - Wrong Checkbox is checked - javascript

With Vue.js, I'm showing the list of items with a checkbox. Clicking on Checkbox will move the item down with strike-through. The issue is, when I click on the checkbox, the wrong checkbox is checked.
For eg, when I click on Apple checkbox, orange checkbox is checked.
Fiddle: https://jsfiddle.net/d6encxe1/
Here's my code,
var myApp = new Vue({
el: '#myApp',
data: {
lists: [
{title: 'Apple', isChecked: false},
{title: 'Orange', isChecked: false},
{title: 'Grapes', isChecked: false}
]
},
computed: {
filterLists: function(){
return _.orderBy(this.lists, ['isChecked', false]);
}
},
methods: {
completeTask: function(e, i){
e.preventDefault();
this.lists[i].isChecked = !this.lists[i].isChecked;
}
}
})
.completed{
text-decoration: line-through;
color: red;
}
<script src="https://unpkg.com/vue"></script>
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
<div id="myApp">
<ul>
<li v-for="(list, index) in filterLists">
<input type="checkbox" v-bind:id="'todo-' + index" v-on:change="completeTask($event, index)" />
<span class="title" v-bind:class="{completed: list.isChecked}">{{list.title}}</span>
</li>
</ul>
</div>

Use a key. A key uniquely identifies an element in Vue. If you do not use a key, Vue will try to re-use existing elements for performance.
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.
You should always use a key when rendering list. Here I'm using the title of your list items, but ideally you should generate a unique key.
<li v-for="(list, index) in filterLists" :key="list.title">
Also you do not need to pass indexes around. Just pass the item itself.
v-on:change="completeTask(list)"
And in completeTask, check it off.
completeTask: function(task){
task.isChecked = !task.isChecked
}
Finally, iterate over your li element and not your ul element.
Updated fiddle.

The index screws things up, you can fix it by binding the checked state
<input type="checkbox" v-bind:checked="list.isChecked" v-bind:id="'todo-' + index" v-on:change="completeTask($event, index)" />
And changing the complete task to just pass the item in:
<input type="checkbox" v-bind:id="'todo-' + index" v-on:change="completeTask(list)" v-bind:checked="list.isChecked" />
See fiddle:
https://jsfiddle.net/d6encxe1/3/
You should remove the 'id' or use something other than the index of the loop because when you re-order the index doesn't change.

You probably want to put v-for in <li> instead of <ul>, or your will get several <ul> elements.
And you didn't provide a key. You should provide a unique key for the items. For example, if the title is unique, you can use it as the key, or you may need to add another attribute like id.
Besides, you can pass the entire list item to the method instead of just the index, because the indexes are changeable in your case:
v-on:change="completeTask($event, list)"
Working example here: https://jsfiddle.net/0r2yb0z6/1/

Related

Vue.js toggle class on click with v-for

How do you toggle a class in vue.js for list rendered elements? This question is an extension on this well answered question. I want to be able to toggle each element individually as well as toggle them all. I have attempted
a solution with the below code but it feels fragile and doesn't seem to work.
A different solution would be to use a single variable to toggle all elements and then each element has a local variable that can be toggled on and off but no idea how to implement that..
// html element
<button v-on:click="toggleAll"></button>
<div v-for="(item, i) in dynamicItems" :key=i
v-bind:class="{ active: showItem }"
v-on:click="showItem[i] = !showItem[i]">
</div>
//in vue.js app
//dynamicItems and showItem will be populated based on API response
data: {
dynamicItems: [],
showItem: boolean[] = [],
showAll: boolean = false;
},
methods: {
toggleAll(){
this.showAll = !this.showAll;
this.showItem.forEach(item => item = this.showAll);
}
}
Here is the small example to acheive you want. This is just a alternative not exact copy of your code.
var app = new Vue({
el:'#app',
data: {
dynamicItems: [
{id:1,name:'Niklesh',selected:false},
{id:2,name:'Raut',selected:false}
],
selectedAll:false,
},
methods: {
toggleAll(){
for(let i in this.dynamicItems){
this.dynamicItems[i].selected = this.selectedAll;
}
}
}
});
.active{
color:blue;
font-size:20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.9/vue.js"></script>
<div id="app">
<template>
<input type="checkbox" v-model="selectedAll" #change="toggleAll"> Toggle All
<div v-for="(item, i) in dynamicItems">
<div :class='{active:item.selected}'><input type="checkbox" v-model="item.selected">Id : {{item.id}}, Name: {{item.name}}</div>
</div>
{{dynamicItems}}
</template>
</div>
I think all you need to do is this
v-bind:class="{ active: showItem || showAll }"
and remove the last line from toggleAll
You also need to use Vue.set when updating array values, as array elements aren't reactive.

Adding element to array that has v-model causes duplicate

I've got a list of text input-fields created through a v-for with a v-model to an array. I want to add elements to the array, and thus creating another input-field.
So far all works. The problem is that the new input-fields are somehow all assigned the same index (?) or something else is happening to cause them to display the same value.
I've made this jsfiddle to showcase what I mean. If you press the button twice and then try to edit one of the new input-boxes, then all the new input-boxes will get the edited value. I'd want only the edited input-box to show the input value.
I guess there is something I am overlooking here. Is there anyone who can help with this please?
Javascript:
new Vue({
el: '#app',
data: {
items: [{name: "one", id: 0}],
template: {
name: "two",
id: 2,
},
},
methods: {
addRow: function(){
this.items.push(this.template);
this.items[this.items.length - 1].id = Math.random();
}
}
})
HTML:
<script src="https://unpkg.com/vue"></script>
<div id="app">
<div v-for="(item,index) in items" :key="item.id">
<input v-model="item.name">
</div>
<button v-on:click="addRow">
Add row
</button>
<div>Array content: {{items}}</div>
</div>
Usage:
screenshot of what i'm getting
The problem here is that with array.push(declaredObject) you are adding a reference of template so every change will be reflected in all its references.
You must add a new object with the same properties, you can achieve that in many ways, the more common is Object.assign({}, this.template) and the newest one is Destructuring objects {...this.template}. so in your case It should be this.items.push({...this.template})
try
this.items.push({
name: "two",
id: 2,
});
instead of this.items.push(this.template) because template property is reactive and it will affect other properties that use it
check this fiddle

How to get the innerText of rendered list item in VueJS

I want to get the innerText of an item in a rendered list, but accessing it using this.$refs doesn't seem to work. I've also tried to use v-modal and that doesn't seem to work either.
Here's my code:
<div id="simple" v-cloak>
<h1>Clicked word value!</h1>
<ul>
<li v-for="word in wordsList" #click="cw_value" ref="refWord">
{{ word }}
</li>
<h4> {{ clickedWord }} </h4>
</ul>
</div>
var app = new Vue({
el: '#simple',
data: {
clickedWord: '',
wordsList: ['word 1', 'word 2', 'word 3']
},
methods: {
cw_value: function() {
this.clickedWord = this.$refs.refWord.innerText
// "I don't know how to get inner text from a clicked value"
}
}
})
Since you've used ref="refWord" on the same element as a v-for, this.$refs.refWord is an array containing each DOM element rendered by v-for.
You should reference the index of each word, and then pass that to the click handler:
<li v-for="word, index in wordsList" #click="cw_value(index)" ref="refWord">
Then, in your cw_value method, use the index value to access the correct element in the array:
cw_value: function(index) {
this.clickedWord = this.$refs.refWord[index].innerText;
}
Here's a working fiddle.
Alternatively, it would be much simpler to just set the clicked word inline in the click handler:
<li v-for="word in wordsList" #click="clickedWord = word">
Here's a working fiddle for that too.
Since innerText takes CSS styles into account, reading the value of innerText triggers a reflow to ensure up-to-date computed styles. (Reflows can be computationally expensive, and thus should be avoided when possible.) Here is MDN document on that.
Now it is:
this.$refs.refWord[index].textContent

In Vue.js, change value of specific attribute for all items in a data array

I'm trying to toggle an open class on a list of items in a v-repeat. I only want one list item (the one most recently clicked) to have the class open.
The data being output has a "class" attribute which is a blank string by default. I'm using this to set the class of the list items in the v-repeat like so:
<li v-repeat="dataSet"
v-on="click: toggleFunction(this)"
class="{{ class }}">
{{ itemContent }}
</li>
I'm using v-on="click: toggleFunction(this)" on each item, which lets me change the class for the specific item, but how do I change the class on all the other items?
My current on-click method:
toggleFunction: function(item) {
if (item.class == '') {
// code to remove the `open` class from all other items should go here.
item.class = 'open';
} else {
item.class = '';
}
}
I've tried using a regular jQuery function to strip the classes: that does remove the classes but it doesn't change the item.class attribute, so things get weird once an item gets clicked more than once...
I'm sure there must be a straightforward way to fix this that I'm not seeing, and having to set a class attribute in the data itself feels hacky anyway (but I'll settle for any fix that works).
I just ran into the same issue. I am still learning Vue, but I tackled it using the "v-class" directive. Using v-class, any time an active value is true for a record, it will automatically add the class "open" to the list element. Hope this JSFiddle helps.
<ul>
<li
v-repeat="people"
v-on="click: toggleActive(this)"
v-class="open: active">{{ name }}
</li>
</ul>
new Vue({
el: '#app',
data: {
people: [
{name: 'mike'},
{name: 'joe',active: true},
{name: 'tom'},
{name: 'mary'}
]
},
methods: {
toggleActive: function(person) {
// remove active from all people
this.people.forEach(function(person){
person.$set('active',false);
});
//set active to clicked person
person.$set('active',true);
}
}
});

Angular track by causing wrong element to be targeted

I ran into this issue yesterday and was wondering if anyone had experienced anything similar and/or had an explanation for why this is happening.
Essentially, I have an ngRepeat block where I had been using track by $index (this was necessary for other reasons outside the scope of this issue). Each item in the list fired a method on click that would apply a class to itself (some CSS for exit effect) and then update it's status to be removed from the list.
Adding the class involved using a selector to target the item by an id associated with the argument to the ngClick method - each item would pass its own id. The ngRepeat collection is generated by a method that filters out any collection members with a particular property, which would also be added in the ngClick method.
The issue is that the class is being applied to two elements - the ngClick'ed element as well as the next element in the collection. Only the ngClick'ed element has the property added and is thus removed from the ngRepeat.
Additionally, console.loging the selection shows some interesting results. Notice the selector versus the 0th element in the result set:
This is a simplified example of the controller logic:
$scope.list = [
{ name : "Joe", id : 1},
{ name : "Clark", id : 2},
{ name : "Matt", id : 3},
{ name : "Jimmy", id : 4},
{ name : "Bob", id : 5}
];
$scope.getItems = function() {
return _.reject($scope.list, 'clicked');
};
$scope.selectItem = function(id) {
angular.element('#item-' + id).addClass('selected');
_.each($scope.list, function(item) { if(item.id === id) { item.clicked = true; } });
};
And this is the ngRepeat in the view:
<div ng-repeat="item in getItems() track by $index">
<h5 ng-bind="item.name" id="item-{{item.id}}" ng-click="selectItem(item.id)"></h5>
</div>
Fiddle here: http://jsfiddle.net/h8bLm8pL/3/
To resolve the issue, I tracked by the id property of each collection member instead of the Angular internal $index, like item in getItems() track by item.id. Still, I'm unclear how this could be happening.
I think it is happening because you are creating the id of the element in the loop but before that when a click event is processed it is targeting the previous element. Try this simplified solution.
<div ng-repeat="item in getItems()">
<h5 ng-bind="item.name" ng-click="item.clicked=true" ng-class="{selected: item.clicked}"></h5>
</div>
Demo http://plnkr.co/edit/VD9lXBtr4TyeviWau8l7?p=preview

Categories