select/deselect single element in v-for - vue.js

I'm trying to make an option list with v-for, where you can choose only one option at a time. It works great except I can't unselect the option.
<div id="main">
<ul>
<li
v-for="l in list"
id="l.key"
#click="selectone(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
The JS
new Vue({
el:"#main",
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone: function(k, o) {
for( i = 0; i < this.list.length; i ++ ) {
if(this.list[i].isSelected == true ) {
this.list[i].isSelected = false
}
}
this.list[k].isSelected = !this.list[k].isSelected;
}
}
})
CSS
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
Shouldn't my loop deactivate all options every time I click an element?

Your close. try this: (un tested)
<div id="main">
<ul>
<li
v-for="(l,index) in list"
id="l.key"
#click="selectone(l, index)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
The JS
new Vue({
el:"#main",
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone:function(l, index){
for( i = 0; i < this.list.length; i ++ ) {
this.list[i].isSelected = false
}
l.isSelected = true;
}
}
}
})
to explain you were miss using the variable k in your function. that should be the entire object not the index

In selectone(), you're setting isSelected=false for all list items, and then attempting to toggle the selected list item's isSelected, which was just previously set to false (i.e., the "toggle" would always set isSelected=true for the selected item).
The loop should exclude the key of the selected item:
selectone(key) {
for (let i = 0; i < this.list.length; i++) {
if (this.list[i].key !== key) {
this.list[i].isSelected = false
}
}
// this.toggleSelection(key)
}
But the toggling code itself needs a bug-fix to properly lookup the list item. The first argument to selectone() is the key property of the list item. In order to get the item by key from the list array, you have to search the list, e.g., using Array.prototype.find():
toggleSelection(key) {
const listItem = this.list.find(item => item.key === key)
if (listItem) {
listItem.isSelected = !listItem.isSelected
}
}
new Vue({
el: '#app',
data: {
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
},
methods: {
selectone(key) {
for (let i = 0; i < this.list.length; i++) {
if (this.list[i].key !== key) {
this.list[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const listItem = this.list.find(item => item.key === key)
if (listItem) {
listItem.isSelected = !listItem.isSelected
}
}
}
})
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="l in list"
id="l.key"
#click="selectone(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
Alternatively, you could track the selected index, set it in the item's click-handler, and set the class binding based on the item's index matching the selected index:
// template
<li
v-for="(l, index) in list"
id="l.key"
#click="selectedIndex = index"
v-bind:class="{ selected: index === selectedIndex, notselected: index !== selectedIndex }"
> {{ l.tec }} </li>
// script
export default {
data() {
return {
selectedIndex: -1,
...
}
}
}
new Vue({
el: '#app',
data: {
selectedIndex: -1,
list: [
{key:"0", tec:"html", isSelected:false},
{key:"1", tec:"css", isSelected:false},
{key:"2", tec:"JS", isSelected:false},
{key:"3", tec:"Git", isSelected:false},
{key:"4", tec:"NodeJS", isSelected:false},
{key:"5", tec:"Postgres", isSelected:false}
]
}
})
.selected {
background:lightpink;
}
.notselected {
background:lightblue;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="(l, index) in list"
id="l.key"
#click="selectedIndex = index"
v-bind:class="{ selected : index === selectedIndex, notselected : index !== selectedIndex }"
> {{ l.tec }} </li>
<ul>
</div>

Related

VueJS- Display multiple nested properties in objects

I have a multi-level navigation component in VueJS. I can retrieve and display the first 2 levels but I also need the nested property called name of the children array to be displayed. How can I get this nested property to print out in my for loop?
Here is my code:
<template>
<div class="navigation__main-menu-wrapper">
<ul>
<li
v-for="(item, index) in navItems"
:key="index">
<div class="navigation__main-menu-list-link">
<a class="mainnav-anchor"
:href="item.url">{{ item.name }}</a>
</div>
<ul
class="navigation__submenu">
<li
v-for="(subItem, index) in item.items"
:key="index">
<a
:href="subItem.url"
:title="subItem.name">
<div>
<span>{{subItem.name}}</span>
</div>
<div
v-for="(subItemChild, index) in items.children"
:key="index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
</a>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
export default {
data: function () {
return {
navItems: []
};
},
mounted: function () {
this.onLoadMainNavigation();
},
methods: {
onLoadMainNavigation: function () {
this.$helpers
.getApiCall("/api/", {
type: "mainnavigation",
})
.then((response) => {
const items = [];
Object.values(response.data.data).forEach((item) => {
const subItems = [];
//Check if there is a submenu
if (item.subMainNavigation) {
item.subMainNavigation.forEach((subItem, subItemChild) => {
subItems.push({
name: subItem.name,
class: subItem.class,
children: [{
name: subItemChild.name
}]
});
});
}
items.push({
name: item.name,
url: item.url,
items: subItems
});
});
this.navItems = items;
})
}
}
};
</script>
This is an exmplae of the data that is outputted
[
{
"name":"Charging solutions"
"items":[
{
"name":"By industry",
"class":"by-industry",
"children":[
{
"id":6671,
"name":"Workplaces",
"class":"workplaces"
},
{
"id":6672,
"name":"Retail & hospitality",
"class":"retail"
},
{
"id":6673,
"name":"Commercial parking",
"class":"parking"
},
{
"id":6674,
"name":"Fuel retailers",
"class":"fuel"
},
]
},
{
"name":"Products",
"class":"products",
"children":[
{
"id":204,
"name":"Public chargers",
"class":"public"
},
{
"id":206,
"name":"Accessories",
"class":"accessories"
},
{
"id":4889,
"name":"Smart charging",
"class":"smart"
}
]
}
]
}
]
Just replace items.children by subItem.children in the last nested loop :
<div v-for="(subItemChild, _index) in subItem.children" :key="_index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
I eventually solved this by simply doing
subItems.push(subItem);

Change ordering of a array of objects with up/down buttons

I'm having the following issue:
I want in the frondend to get a list of items based on the key 'order':
<div id="app">
<ul>
<li v-for="item in sorted">
{{item.order}} {{item.title}}
<button #click="changeOrderDown(item)">down</button>
<button #click="changeOrderUp(item)">up</button>
</li>
</ul>
</div>
based on the JSON you see below. When you click the button I want to swap out for example order 1 -> 2 and then 2 becomes 1
items: [
{
title: "test",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
I keep getting a duplicate key error cause i first change the first key and then the second. i resolved it to update the whole object at ones. that seems to work but still it doesn't behave the correct way.
computed: {
sorted() {
function compare(a, b) {
let comparison = 0;
if (a.order > b.order) {
comparison = 1;
} else if (a.order < b.order) {
comparison = -1;
}
return comparison;
}
return this.items.sort(compare)
},
},
methods: {
changeOrderDown(currentItem) {
let temp = this.items
let old_value = parseFloat(currentItem.order)
let new_value = parseFloat(currentItem.order) + 1;
console.log(old_value, new_value)
temp.filter(o => o.order === old_value)[0].order = new_value;
temp.filter(o => o.order === new_value)[0].order = old_value;
this.items = temp;
},
changeOrderUp(currentItem) {
let temp = this.items
let old_value = parseFloat(currentItem.order)
let new_value = parseFloat(currentItem.order) - 1;
console.log(old_value, new_value)
temp.filter(o => o.order === old_value)[0].order = new_value;
temp.filter(o => o.order === new_value)[0].order = old_value;
this.items = temp;
},
}
I made a codepen down below with the code from above. this is kinda a working example but it doesn't feel right. Can someone give me a push in the right direction?
https://codepen.io/frank-derks/pen/BaQVOZV
Interesting challenge. Using my Vue 2 CLI sandbox app, I came up with functionality that doesn't require an 'order' property. Here is the component code:
<template>
<div class="swap-array-objects">
<h3>SwapArrayObjects.vue</h3>
<div class="row">
<div class="col-md-6">
<table class="table table-bordered">
<thead>
<tr>
<th>TITLE</th>
<th> </th>
<th> </th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="index">
<td>{{ item.title }}</td>
<td>
<button class="btn btn-secondary btn-sm" #click="moveUp(index)">Up</button>
</td>
<td>
<button class="btn btn-secondary btn-sm" #click="moveDown(index)">Down</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
title: 'title1'
},
{
title: 'title2'
},
{
title: 'title3'
},
{
title: 'title4'
},
{
title: 'title5'
}
]
}
},
methods: {
moveUp(index) {
if (index === 0) {
return;
}
let priorIndex = index - 1;
let itemCopy = {...this.items[index]};
let priorItemCopy = {...this.items[priorIndex]};
// Swap array position with prior element
this.$set(this.items, priorIndex, itemCopy);
this.$set(this.items, index, priorItemCopy);
},
moveDown(index) {
if (index === this.items.length - 1) {
return;
}
let subsequentIndex = index + 1;
let itemCopy = {...this.items[index]};
let subsequentItemCopy = {...this.items[subsequentIndex]};
// Swap array positions with subsequent element
this.$set(this.items, subsequentIndex, itemCopy);
this.$set(this.items, index, subsequentItemCopy);
}
}
}
</script>
This solution is similar to Tim's, but a bit simpler and easier to follow:
<template>
<v-app>
<ul>
<li v-for="(item, index) in items" :key="index">
{{item.order}} {{item.title}} {{index}}
<button #click="changeOrderDown(item, index)" v-if="index != items.length-1">down</button>
<button #click="changeOrderUp(item, index)" v-if="index != 0">up</button>
</li>
</ul>
</v-app>
</template>
<script>
export default {
name: 'App',
data: () => ({
items: [
{
title: "test1",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
}),
methods: {
changeOrderDown(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index + 1]
// move clicked item to destination
this.items[index + 1] = temp
},
changeOrderUp(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index - 1]
// move clicked item to destination
this.items[index - 1] = temp
},
}
};
</script>
<template>
<v-app>
<ul>
<li v-for="(item, index) in items" :key="index">
{{item.order}} {{item.title}} {{index}}
<button #click="changeOrderDown(item, index)" v-if="index != items.length-1">down</button>
<button #click="changeOrderUp(item, index)" v-if="index != 0">up</button>
</li>
</ul>
</v-app>
</template>
<script>
export default {
name: 'App',
data: () => ({
items: [
{
title: "test1",
order: 1
},
{
title: "test2",
order: 2
},
{
title: "test3",
order: 3
}
]
}),
methods: {
changeOrderDown(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index + 1]
// move clicked item to destination
this.items[index + 1] = temp
},
changeOrderUp(item, index) {
// save clicked item in temporary variable
let temp = item
// move the following item to the clicked element
this.items[index] = this.items[index - 1]
// move clicked item to destination
this.items[index - 1] = temp
},
}
};
</script>

#click bound to each item within v-for loop executed many times when clicking

I have a #click bound to individual items within a v-for loop. In the resulting render, I should have one #click for each item, so clicking one item should trigger the function bound to the item once.
Yet it triggers it as many times as there are items. Why?
<ul>
<li :key="option.value" v-for="option in options">
<QuizCheckbox
#click.native="handleClick(option.value)"
>
{{ option.value }}
</QuizCheckbox>
</li>
</ul>
...
methods: {
handleClick(val) {
console.log(val);
},
EDIT:
If I replaced ... with a simple element, then clicking that doesn't trigger the problem. So it's the <QuizCheckbox> component who's the culprit. However, nothing in it seems to indicate what could cause the problem. Here's the content of QuizCheckbox.vue:
<template>
<div :class="['quiz-checkbox', {'quiz-checkbox--checked': shouldBeChecked}]">
<div :class="['form-checkbox']">
<label class="form-checkbox__label">
<slot/>
<input
:checked="shouldBeChecked"
:value="value"
#change="updateInput"
class="form-checkbox__input"
type="checkbox"
/>
<span class="form-checkbox__checkmark"></span>
</label>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'modelValue',
event: 'change'
},
props: {
value: {
type: String,
},
modelValue: {
type: [Boolean, Array],
default: false
}
},
computed: {
shouldBeChecked() {
if (this.modelValue instanceof Array) {
return this.modelValue.includes(this.value);
}
return this.modelValue;
}
},
created() {
if (!this.$slots.default) {
console.error('QuizCheckbox: requires label to be provided in the slot');
}
},
methods: {
updateInput(event) {
const isChecked = event.target.checked;
if (this.modelValue instanceof Array) {
const newValue = [...this.modelValue];
if (isChecked) {
newValue.push(this.value);
} else {
newValue.splice(newValue.indexOf(this.value), 1);
}
this.$emit('change', newValue);
} else {
this.$emit('change', isChecked);
}
}
}
};
</script>
The code you post seems fine. Although simplified here is the code you post running:
new Vue({
el: 'ul',
data: {
options: [
{
value: 'option1',
},
{
value: 'option2',
}
]
},
methods: {
handleClick(val) {
console.log(val);
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<ul>
<li :key="option.value" v-for="option in options">
<div
#click="handleClick(option.value)"
>
{{ option.value }}
</div>
</li>
</ul>
The problem must be elsewhere.

Vue-MultiSelect Checkbox binding

The data properties of the multi-select component does not update on change. Check-boxes doesn't update on the front-end.
Expected Behavior: The check-boxes should get ticked, when clicked.
Link to code: https://jsfiddle.net/bzqd19nt/3/
<div id="app">
<multiselect
select-Label=""
selected-Label=""
deselect-Label=""
v-model="value"
:options="options"
:multiple="true"
track-by="library"
:custom-label="customLabel"
:close-on-select="false"
#select=onSelect($event)
#remove=onRemove($event)
>
<span class="checkbox-label" slot="option" slot-scope="scope" #click.self="select(scope.option)">
{{ scope.option.library }}
<input class="test" type="checkbox" v-model="scope.option.checked" #focus.prevent/>
</span>
</multiselect>
<pre>{{ value }}</pre>
</div>
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
data: {
value: [],
options: [
{ language: 'JavaScript', library: 'Vue.js', checked: false },
{ language: 'JavaScript', library: 'Vue-Multiselect', checked: false },
{ language: 'JavaScript', library: 'Vuelidate', checked: false }
]
},
methods: {
customLabel(option) {
return `${option.library} - ${option.language}`;
},
onSelect(option) {
console.log('Added');
option.checked = true;
console.log(`${option.library} Clicked!! ${option.checked}`);
},
onRemove(option) {
console.log('Removed');
option.checked = false;
console.log(`${option.library} Removed!! ${option.checked}`);
}
}
}).$mount('#app');
In your code you call onSelect and try to change the option argument of this function inside the function:
option.checked = true;
This affects only the local variable option (the function argument). And doesn't affect objects in options array in the data of the Vue instance, the objects bound with checkboxes. That's why nothing happens when you click on an option in the list.
To fix it find the appropriate element in options array and change it:
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = true;
Here is the code snippet with fix:
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
data: {
value: [],
options: [
{ language: 'JavaScript', library: 'Vue.js', checked: false },
{ language: 'JavaScript', library: 'Vue-Multiselect', checked: false },
{ language: 'JavaScript', library: 'Vuelidate', checked: false }
]
},
methods: {
customLabel (option) {
return `${option.library} - ${option.language}`
},
onSelect (option) {
console.log("Added");
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = true;
console.log(option.library + " Clicked!! " + option.checked);
},
onRemove (option) {
console.log("Removed");
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = false;
console.log(option.library + " Removed!! " + option.checked);
}
}
}).$mount('#app')
* {
font-family: 'Lato', 'Avenir', sans-serif;
}
.checkbox-label {
display: block;
}
.test {
position: absolute;
right: 1vw;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link href="https://unpkg.com/vue-multiselect#2.0.2/dist/vue-multiselect.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue-multiselect#2.0.3/dist/vue-multiselect.min.js"></script>
<div id="app">
<multiselect
select-Label=""
selected-Label=""
deselect-Label=""
v-model="value"
:options="options"
:multiple="true"
track-by="library"
:custom-label="customLabel"
:close-on-select="false"
#select=onSelect($event)
#remove=onRemove($event)
>
<span class="checkbox-label" slot="option" slot-scope="scope" #click.self="select(scope.option)">
{{ scope.option.library }}
<input class="test" type="checkbox" v-model="scope.option.checked" #focus.prevent/>
</span>
</multiselect>
<pre>{{ value }}</pre>
</div>

How can I add condition in array on vue.js 2?

My vue component is like this :
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': currentPath === '/message/inbox' }"
>
Message
</a>
If they meet the conditions then the message menu will be active
But, I want to make it like this :
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': currentPath in array ('/message/inbox', '/message/inbox/detail') }"
>
Message
</a>
So it will check currentPath in the array
How can I do it?
Update :
If I have menu again like this :
<a :href="baseUrl+'/store/sale'"
:class="{ 'active': currentPath in array ('/store/sale', '/store/sale/detail') }"
>
Sale
</a>
Or more menu
How to implement it?
Update 2
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': isActive }"
>
Message
</a>
<a :href="baseUrl+'/store/sale'"
:class="{ 'active': isActiveSale }"
>
Message
</a>
computed: {
isActive () {
return ['/message/inbox', '/message/inbox/detail'].indexOf(this.currentPath) > -1
},
isActiveSale () {
return ['/store/sale', '/store/sale/detail'].indexOf(this.currentPath) > -1
}
}
You can use computed properties :
computed: {
currentPathInInbox: function() {
var arrayInbox = ['/message/inbox', '/message/inbox/detail'];
return arrayInbox.indexOf(this.currentPath) > -1;
}
}
and in template :
:class="{ 'active': currentPathInInbox }"
or with no computed properties :
:class="{ 'active': (currentPath === '/message/inbox' || (currentPath === '/message/inbox/detail') }"
UPDATED :
I think you need component :
Vue.component( 'linkWithPath', {
template: '<div><a :href="baseUrl + relativeUrl"' +
':class="{ \'active\': isActive }">' +
'<slot>Link name</slot></a></div>',
props: {
baseUrl: { type: String },
currentPath: { type: String, default: '' },
relativeUrl: { type: String }
},
computed: {
isActive: function() {
return [ this.relativeUrl, this.relativeUrl + '/detail'].indexOf(this.currentPath) > -1;
}
}
});
Vue.component( 'listOfLinksWithPath', {
template: '<div><link-with-path v-for="menuItem in menuArray"' +
':key="menuItem" :base-url="baseUrl" :current-path="currentPath"' +
':relative-url="menuItem.url">{{ menuItem.name }}</link-with-path></div>',
props: {
baseUrl: { type: String },
currentPath: { type: String },
menuArray: { type: Array }
}
});
new Vue({
el: "#app",
data: function() {
return {
baseUrl: 'http://www.CHANGETHISURL.com',
currentPath: '/message/inbox',
menuArray: [ { name: 'Message', url: '/message/inbox' },
{ name: 'Sale', url: '/store/sale' } ]
}
},
methods: {
changeCurrentPath: function() {
this.currentPath = '/store/sale'
}
}
});
a.active{
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.5/vue.js"></script>
<div id="app">
<p>Link is active when it is red.</p>
<list-of-links-with-path :base-url="baseUrl" :menu-array="menuArray" :current-path="currentPath"></list-of-links-with-path>
<br />
<button #click="changeCurrentPath" type="button">Change current path</button>
<br />
currentPath : {{ currentPath }}
</div>
Add a computed property.
computed: {
isActive () {
return ['/message/inbox', '/message/inbox/detail'].indexOf(this.currentPath) > -1
}
}
So you'll be able to use:
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': isActive }"
>
Message
</a>