How can I toggle one state only and undo it when I toggle the next (VueJS) - vue.js

First, I apologize for the way I phrased the question. I couldn't find a way to word it in one sentence. So, allow me to explain.
I have an API with thousands of items being looped in a v-for. The user will select one and only one at a time from the collection. And upon clicking on it will show on a separate place along with some other data (* so it's not a case of string interpolation. It's an object. I'm just simplifying the code in my jsfiddle to make it less confusing and run without dependencies)
I added a boolean property to the API to toggle it true/false and a method function that does the toggle. And with the help of some v-if and v-show directives I'm hiding the other elements from rendering when they are false.
<div id="app">
<div uk-grid class="card-body">
<div class="uk-width-1-4#m">
<div>1. Select an Item</div>
<div class="pods" v-for="pod in pods" :key="pod.id" :id="pod.id">
<div class="ws-in-col" v-for="workstations in pod.workstations" :key="workstations.id" :id="workstations.id" #click="selectWS(workstations, pod)">
<div class="ws ws-number uk-text-default">{{workstations.number}}</div>
</div>
</div>
</div>
<div class="uk-width-1-4#m">
<div>2. Selected Item</div>
<div class="pods" v-for="pod in pods" :key="pod.id" :id="pod.id" v-show="pod.selected===true">
<div class="ws-in-col" v-for="workstations in pod.workstations" :key="workstations.id" :id="workstations.id" v-show="workstations.selected===true">
<div v-if="workstations.selected === true">
<div class="group">
<div class="ws ws-number uk-text-default">{{workstations.number}}</div>
<div class="ws ws-number uk-text-default">{{workstations.text}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
and in the script
methods: {
selectWS(workstations, pods) {
pods.selected = !pods.selected;
workstations.selected = !workstations.selected;
}
}
However, not only it is very messy and rookie. It's buggy. The only way it works is if the user clicks on one item to show it, and clicks it again to toggle it off before clicking another one to turn it on. That's far from user-friendly.
How can I resolve this in a cleaner and professional way so that if the user clicks on 1.1, it shows 1.1 and if he wants to see 1.2, all he has to do is click on 1.2 without having to turn off 1.1 first?
Here's a JSFIDDLE replicating my problem
Thanks guys. Being the only Vue dev in this place is tough.

Agree with IVO, instead of tracking in-item, use a value(or two) to track selection
The only reason I'm posting separate answer is that I'd recommend using a computed value if you need to have the selected instance available as an object.
new Vue({
el: "#app",
data: {
workstation: null,
pod: null,
pods: [
{
id: "pod1",
number: 1,
selected: false,
"workstations": [
{
id: "ws11",
number: "1.1",
text: "Some text",
selected: false,
close: false
},
{
id: "ws12",
number: "1.2",
text: "Some text",
selected: false,
close: false
},
{
id: "ws13",
number: "1.3",
text: "Some text",
selected: false,
close: false
}
]
},
{
id: "pod2",
number: 2,
selected: false,
"workstations": [
{
id: "ws21",
number: "2.1",
text: "Some text",
selected: false,
close: false
},
{
id: "ws22",
number: "2.2",
text: "Some text",
selected: false,
close: false
},
{
id: "ws23",
number: "2.3",
text: "Some text",
selected: false,
close: false
}
]
}
]
},
computed: {
selection(){
if(this.workstations !== null && this.pod !== null){
let s = this.pods.filter(p => p.id === this.pod).map(p => {
let r = {...p}
r.workstations = p.workstations.filter(w => w.id === this.workstation)
return r
})
if (s.length === 1) return s[0]
}
return false
}
},
methods: {
selectWS(workstations, pods) {
if (this.pod == pods.id && this.workstation == workstations.id){
this.pod = null;
this.workstation = null;
} else{
this.pod = pods.id;
this.workstation = workstations.id;
}
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.1.7/js/uikit.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.1.7/css/uikit.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<div uk-grid class="card-body">
<div class="uk-width-1-4#m">
<div>1. Select an Item</div>
<div class="pods" v-for="pod in pods" :key="pod.id" :id="pod.id">
<div class="ws-in-col" v-for="workstations in pod.workstations" :key="workstations.id" :id="workstations.id" #click="selectWS(workstations, pod)">
<div class="ws ws-number uk-text-default">{{workstations.number}}</div>
</div>
</div>
</div>
<div class="uk-width-1-4#m">
<div>2. Selected Item</div>
<div v-if="selection" class="pods">
<div class="ws-in-col">
<div v-if="selection.workstations.length > 0">
<div class="group">
<div class="ws ws-number uk-text-default">{{selection.workstations[0].number}}</div>
<div class="ws ws-number uk-text-default">{{selection.workstations[0].text}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

You have to simply set the last selected pod/workstation and then show its properties (please run the snippet in full page otherwise you will be unable to see the right column):
new Vue({
el: "#app",
data: {
selectedPod: null,
selectedWorkstation: null,
pods: [
{
id: "pod1",
number: 1,
selected: false,
"workstations": [
{
id: "ws11",
number: "1.1",
text: "Some text 1",
selected: false,
close: false
},
{
id: "ws12",
number: "1.2",
text: "Some text 2",
selected: false,
close: false
},
{
id: "ws13",
number: "1.3",
text: "Some text 3",
selected: false,
close: false
}
]
},
{
id: "pod2",
number: 2,
selected: false,
"workstations": [
{
id: "ws21",
number: "2.1",
text: "Some text 4",
selected: false,
close: false
},
{
id: "ws22",
number: "2.2",
text: "Some text 5",
selected: false,
close: false
},
{
id: "ws23",
number: "2.3",
text: "Some text 6",
selected: false,
close: false
}
]
}
]
},
methods: {
selectWS(workstation, pod) {
this.selectedPod = pod;
this.selectedWorkstation = workstation;
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.1.7/js/uikit.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.1.7/css/uikit.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div uk-grid class="card-body">
<div class="uk-width-1-4#m">
<div>1. Select an Item</div>
<div class="pods" v-for="pod in pods" :key="pod.id">
<div class="ws-in-col" v-for="workstation in pod.workstations" :key="workstation.id" #click="selectWS(workstation, pod)">
<div class="ws ws-number uk-text-default">
{{workstation.number}}
{{workstation.text}}
</div>
</div>
</div>
</div>
<div class="uk-width-1-4#m">
<div>2. Selected Item</div>
<div class="pods" v-if="selectedWorkstation">
<div class="ws-in-col">
<div>
<div class="group">
<div class="ws ws-number uk-text-default">{{selectedWorkstation.number}}</div>
<div class="ws ws-number uk-text-default">{{selectedWorkstation.text}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

Related

VueDraggable multiDrag and selected-class props not work in Nuxt.js

I currently use vue-draggable to make drag and drop component in my latest Nuxt project.
My package.json is like that (with the latest version of vuedraggable)
"nuxt": "^2.14.12",
"vuedraggable": "^2.24.3",
// I also use those plugins
"#nuxtjs/vuetify": "^1.11.3",
First of all, I tried to make mini-sample referred to this sample.
And I made code like below
I just changed :list to v-model in draggable tag, and use nuxt-property-decorator instead of export default. referred to latest vue-draggable
<template>
<div id="app">
<div class="flex">
<div class="flex-1">
Target Category Items: {{ targetCategoryItems.length }}
<div class="draggable-container">
<draggable
v-model="targetCategoryItems"
draggable=".element"
group="elements"
:multi-drag="true"
class="draggable"
selected-class="selected-item"
>
<div
v-for="element in targetCategoryItems"
:key="element.ItemID"
class="draggable-element"
>
<div class="item-text">
<div>[{{ element.ItemID }}]</div>
<div>{{ element.ItemName }}</div>
</div>
</div>
</draggable>
</div>
</div>
<div class="flex-1">
Source Category Items: {{ sourceCategoryItems.length }}
<div class="draggable-container">
<draggable
v-model="sourceCategoryItems"
draggable=".element"
group="elements"
:multi-drag="true"
class="draggable"
selected-class="selected-item"
#select="selectItems"
>
<div
v-for="element in sourceCategoryItems"
:key="element.ItemID"
class="draggable-element"
>
<div class="item-text">
<div>[{{ element.ItemID }}]</div>
<div>{{ element.ItemName }}</div>
</div>
</div>
</draggable>
</div>
</div>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
export default {
components: { draggable },
data() {
return {
sourceCategoryItems: [
{ ItemID: "566GR", ItemName: "Leaf Urn", PhotoName: "" },
{ ItemID: "575GD", ItemName: "Italian Villa Planter", PhotoName: "" },
{ ItemID: "576GR", ItemName: "Palm Topiary Planter", PhotoName: "" },
],
targetCategoryItems: [
{ ItemID: "F238", ItemName: "Cuadrado Side Table", PhotoName: "" },
{ ItemID: "F239", ItemName: "Triangulo Side Table", PhotoName: "" },
{ ItemID: "F285", ItemName: "Kew Occassional Table", PhotoName: "" },
{ ItemID: "F286", ItemName: "Tuileries Coffee Table", PhotoName: "" },
{ ItemID: "F296", ItemName: "Heligan Table", PhotoName: "" },
],
};
},
methods: {
toggle(todo) {
todo.done = !todo.done;
},
selectItems(event) {
console.log(event.items);
},
},
};
</script>
<style scoped>
.flex {
display: flex;
}
.flex-1 {
flex: 1;
}
.draggable-container {
border: 1px solid black;
padding: 10px;
margin: 10px;
}
.draggable-element {
padding: 10px;
margin: 10px;
background-color: lightgrey;
}
.selected-item {
background-color: red;
opacity: 0.5;
}
</style>
The result is like that
It seems that there is no problem, but I clicked the left side table, background-color never changed and I couldn't drag and drop any items.
I also checked the same issues, "SortableJS / Vue.Draggable multi-drag option not working" and "Use with Nuxt? #525". So I tried to make drag.js in src/plugins, but it doesn't work either.
So, how can I correctly use muli-drag and selected-class props in Nuxt?
This is issues Github repository.

nuxtjs add and remove class on click on elements

I am new in vue and nuxt and here is my code I need to update
<template>
<div class="dashContent">
<div class="dashContent_item dashContent_item--active">
<p class="dashContent_text">123</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">456</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">789</p>
</div>
</div>
</template>
<style lang="scss">
.dashContent {
&_item {
display: flex;
align-items: center;
}
&_text {
color: #8e8f93;
font-size: 14px;
}
}
.dashContent_item--active {
.dashContent_text{
color:#fff;
font-size: 14px;
}
}
</style>
I tried something like this:
<div #click="onClick">
methods: {
onClick () {
document.body.classList.toggle('dashContent_item--active');
},
},
but it changed all elements and I need style change only on element I clicked and remove when click on another
also this code add active class to body not to element I clicked
This is how to get a togglable list of fruits, with a specific class tied to each one of them.
<template>
<section>
<div v-for="(fruit, index) in fruits" :key="fruit.id" #click="toggleEat(index)">
<span :class="{ 'was-eaten': fruit.eaten }">{{ fruit.name }}</span>
</div>
</section>
</template>
<script>
export default {
name: 'ToggleFruits',
data() {
return {
fruits: [
{ id: 1, name: 'banana', eaten: false },
{ id: 2, name: 'apple', eaten: true },
{ id: 3, name: 'watermelon', eaten: false },
],
}
},
methods: {
toggleEat(clickedFruitIndex) {
this.fruits = this.fruits.map((fruit) => ({
...fruit,
eaten: false,
}))
return this.$set(this.fruits, clickedFruitIndex, {
...this.fruits[clickedFruitIndex],
eaten: true,
})
},
},
}
</script>
<style scoped>
.was-eaten {
color: hsl(24, 81.7%, 49.2%);
}
</style>
In Vue2, we need to use this.$set otherwise, the changed element in a specific position of the array will not be detected. More info available in the official documentation.

Vue JS click event not firing up

I'm developing app where I have a dynamic .ddlist for each .box.
<div id="app">
<h2>MY LIST</h2>
<div class="box">
<input v-on:focus="showDDList" v-on:blur="showDDList" type="text" value="" placeholder="hello list">
<ul class="ddlist">
<li #click.prevent="myalert()" v-for="item in hello">{{item.text}}</li>
</ul>
</div>
<div class="box">
<input v-on:focus="showDDList" v-on:blur="showDDList" type="text" value="" placeholder="there list">
<ul class="ddlist">
<li #click.prevent="myalert()" v-for="item in there">{{item.text}}</li>
</ul>
</div>
</div>
What I want to achieve is when user focus on input, very next .ddlist should be visible. Here is Javascript part :
new Vue({
el: "#app",
data: {
hello: [
{ text: "hello 1" },
{ text: "hello 2" },
{ text: "hello 3" },
{ text: "hello 4" }
],
there: [
{ text: "there 1" },
{ text: "there 2" },
{ text: "there 3" },
{ text: "there 4" }
]
},
methods: {
showDDList: function(e) {
e.target.nextElementSibling.classList.toggle('active');
},
myalert: function() {
alert("Hello world");
},
}
})
My code is working fine until here and .ddlist does show up on focusing on input
What Next I want to do is When a user clicks on .ddlist child a, It should fire #click event binded to it.
There is a problem. Now when user clicks on a nothing shows up.
What I notice is because I'm toggling with css display:block/none of .ddlist, #click event is not firing up.
Here is the working fiddle : https://jsfiddle.net/6y9mwh3u/
May be you can throw some light on achieving my result with alternative ?
You can use mousedown event listener instead in order to execute myalert before showDDList since blur was preventing click from being called.
new Vue({
el: "#app",
data: {
hello: [
{ text: "hello 1" },
{ text: "hello 2" },
{ text: "hello 3" },
{ text: "hello 4" }
],
there: [
{ text: "there 1" },
{ text: "there 2" },
{ text: "there 3" },
{ text: "there 4" }
]
},
methods: {
showDDList: function(e) {
e.target.nextElementSibling.classList.toggle('active');
},
myalert: function() {
alert("Hello world");
},
}
})
.box {
display: block;
padding: 10px;
margin: 10px;
border: 1px #ddd solid;
}
.ddlist {
display: none;
margin-top : 10px;
}
.ddlist.active {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>MY LIST</h2>
<div class="box">
<input v-on:focus="showDDList" v-on:blur="showDDList" type="text" value="" placeholder="hello list">
<ul class="ddlist">
<li #mousedown.prevent="myalert" v-for="item in hello">{{item.text}}</li>
</ul>
</div>
<div class="box">
<input v-on:focus="showDDList" v-on:blur="showDDList" type="text" value="" placeholder="there list">
<ul class="ddlist">
<li #mousedown.prevent="myalert" v-for="item in there">{{item.text}}</li>
</ul>
</div>
</div>

Toggle form inside v-for using vue.js

How can i toggle form inside v-for loop,I have a form inside v-for which i want to display (toggle) on click.
But when i click all the form inside the v-for gets toggled.
Secondly is it better approach to keep the form inside the loop,when have large amount of data inside loop or load it as a separate component.
This is what i am trying to do.
new Vue({
el: "#app",
data: {
todos: [{
text: "Learn JavaScript"
},
{
text: "Learn Vue"
},
{
text: "Play around in JSFiddle"
},
{
text: "Build something awesome"
}
],
show: ''
},
methods: {
toggle: function(todo) {
this.show = !this.show
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="(todo,key) in todos">
<p>
{{ key+1 }} - {{ todo.text}} <span #click="toggle(todo)"><b>Contact</b></span>
<div v-if="show">
<hr />
<p>
<label>Message</label>
<input type="text">
</p>
<hr />
</div>
</p>
</li>
</ol>
</div>
There is only 1 reactive variable show. Setting it to true while all form is using v-if="show", will show everything.
You can set show to something that each form uniquely have. For example, its text, and perform a v-if using its text.
demo: https://jsfiddle.net/jacobgoh101/umaszo9c/
change v-if="show" to v-if="show === todo.text"
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="(todo,key) in todos">
<p>
{{ key+1 }} - {{ todo.text}} <span #click="toggle(todo)"><b>Contact</b></span>
<div v-if="show === todo.text">
<hr />
<p>
<label>Message</label>
<input type="text">
</p>
<hr />
</div>
</p>
</li>
</ol>
</div>
change toggle method
new Vue({
el: "#app",
data: {
todos: [{
text: "Learn JavaScript"
},
{
text: "Learn Vue"
},
{
text: "Play around in JSFiddle"
},
{
text: "Build something awesome"
}
],
show: ''
},
methods: {
toggle: function(todo) {
if (this.show === todo.text)
this.show = false
else
this.show = todo.text
}
}
})
property "show" should be a prop of todo,not prop of data
new Vue({
el: "#app",
data: {
todos: [{
text: "Learn JavaScript"
},
{
text: "Learn Vue"
},
{
text: "Play around in JSFiddle"
},
{
text: "Build something awesome"
}
].map(o=>({...o,show:false}))
},
methods: {
toggle: function(todo) {
todo.show = !todo.show
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="(todo,key) in todos">
<p>
{{ key+1 }} - {{ todo.text}} <span #click="toggle(todo)"><b>Contact</b></span>
<div v-if="todo.show">
<hr />
<p>
<label>Message</label>
<input type="text">
</p>
<hr />
</div>
</p>
</li>
</ol>
</div>

Vue.js 2.0 data binding issue - one data field's change will trigger the other data fields' filter

Following is the code, I define a my data in Vue as below:
{
result: {
name: 'user A',
age: 20,
male: true
},
showDetails: true
}
There are several filter/computed/methods in the view will do the result formatting.
The problem is even I just change the value of the showDetailds, the methods and filters defined for the result will be triggered as well.
May I know any issue in my code ? Or it's the natural of Vue.js (I don't believe.) ?
function countTheCall(key) {
let counter = document.getElementById(key)
if (!counter) {
let container = document.getElementById('statList')
var item = document.createElement('li')
item.innerHTML = `${key}: <span id='${key}'>0</span>`
container.appendChild(item)
counter = document.getElementById(key)
}
counter.innerText = _.parseInt(counter.innerText) + 1
}
var vm = new Vue({
el: '#app',
data() {
return {
result: {
name: 'user A',
age: 20,
male: true
},
showDetails: true
}
},
methods: {
countTheCall: function(key) {
let counter = document.getElementById(key)
counter.innerText = _.parseInt(counter.innerText) + 1
},
trimValue: function(value) {
console.log('Invoke method - trimValue.')
countTheCall('methodCalls of trimValue')
return _.trim(value)
},
getValFromResult: function(key) {
console.log('Invoke method - getValFromResult.')
countTheCall('methodCalls of getValFromResult')
return _.get(this.result, key, 'null')
}
},
computed: {
displayName: function() {
console.log('Invoke computed value - computedVal.')
countTheCall('computedCalls of displayName')
return `${this.result.name} (${this.result.age})`
}
},
filters: {
convertString: function(val) {
countTheCall('filterCalls of convertString')
return _.upperFirst(val)
},
getValFromObject: function(obj, key) {
console.log(`[filter] getValue From Object by ${key}`)
countTheCall('filterCalls of getValFromObject')
return _.get(obj, key, 'null')
},
convertDateString: function(datestr, format = 'MMM DD (ddd), YYYY') {
console.log(`[filter] convertDateString ${datestr}`)
countTheCall('filterCalls of convertDateString')
let m = moment(datestr)
let formattedStr = ''
if (m.isValid()) {
formattedStr = m.format(format)
}
return formattedStr
}
}
})
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
label {
font-style: italic;
color: black;
}
span {
font-weight: bold;
color: darkblue;
}
.callStat {
float: right;
width: 400px;
border: solid darkgrey 2px;
padding: 5px;
margin-right: 40px;
}
<body>
<script type="text/javascript" src="//cdn.bootcss.com/vue/2.2.4/vue.min.js"></script>
<script type="text/javascript" src="//cdn.bootcss.com/moment.js/2.17.1/moment-with-locales.min.js"></script>
<script type="text/javascript" src="//cdn.bootcss.com/lodash.js/4.17.4/lodash.min.js"></script>
<div class="callStat">
<h4>The Stat. of Calls</h4>
<ul id="statList"></ul>
</div>
<div id="app">
<h2>User Profile</h2>
<div>
<label>User Name:</label>
<input type="text" v-model="result.name"></input>
</div>
<div>
<label>Show Details:</label>
<input type="checkbox" id="showDetails" v-model="showDetails"></input>
<label for="showDetails">show detail section?</label>
</div>
<hr/>
<div>
<label>User Name:</label>
<ul>
<li>result.name: <span>{{trimValue(result.name)}}</span></li>
<li>result.unknownFiled: <span>{{result.unknownFiled}}</span></li>
<li>getValFromResult('name'): <span>{{ getValFromResult('name')}} </span></li>
<li>getValFromResult('unknownField'): <span>{{ getValFromResult('unknownField')}} </span></li>
<li>computed Display Name : <span>{{ displayName }} </span></li>
<li>convertString by filter : <span>{{ result.name | convertString }} </span></li>
<li>user birthDay: <span>{{ result.birthDay | convertDateString}}</span></li>
</ul>
</div>
</div>
</body>
This is natural to Vue.
Methods & Filters run on every DOM re-render.
This is why it's best to use Computed whenever possible - these re-run only on change of connected properties.