Vue.js Problem Controlling Div with v-show - vue.js

I have four buttons and corresponding four divisions. On clicking first button, I want to display first div, on clicking the second button, I want to display the second div, and so on.
The button click event calls a method with the division number (0 through 3)
#click="showDiv(0)" for first button and #click="showDiv(1)" for the second button. This is my showDiv() method
showDiv: function(divNumber)
showDiv: function(divNumber)
{
this.showDetailsDiv.forEach(function(item, index, array) {
array[index]=null;
});
this.showDetailsDiv[divNumber] = true;
console.log(this.ShowDetailsDiv);
}
The initial value of showDetailsDiv prop is array with null values
showDetailsDiv: [
null,
null,
null,
null
],
In template, I am trying to control the divisions through v-show
<div v-show="showDetailsDiv[0]">
First div
</div>
<div v-show="showDetailsDiv[1]">
Second div
</div>
<div v-show="showDetailsDiv[2]">
Third div
</div>
When I click the button, I see that the corresponding element of showDetailsDiv is changing to true, however, the corresponding division does not display. Is there anything wrong in my logic?
When I try to control the display of division using direct properties (e.g. showDiv0, showDiv1, showDiv2 & showDiv3) with the following code, it works.
showDiv: function(divNumber)
{
this.showDetailsDiv.forEach(function(item, index, array) {
array[index]=null;
});
this.showDetailsDiv[divNumber] = true;
console.log(this.showDetailsDiv);
this.showDiv0= false;
this.showDiv1= false;
this.showDiv2= false;
this.showDiv3= false;
let elementID = 'showDiv' + divNumber;
this[elementID] = true;
}
<div v-show="showDiv0">
First div
</div>
<div v-show="showDiv1">
Second div
</div>
<div v-show="showDiv2">
Third div
</div>
Any suggestions?

Keep it simple. Template should look like:
<div v-show="visible == 0">
First div
</div>
<div v-show="visible == 1">
Second div
</div>
<div v-show="visible == 2">
Third div
</div>
data() {
return {
visible: null,
}
}
You can simplify showDiv method to:
showDiv: function(divNumber) {
this.visible = divNumber;
}
You can also add method isVisible to check which div is visible:
isVisible(divNumber) {
return this.visible == divNumber;
}
and use it like:
<div v-show="isVisible(0)">
First div
</div>

If you change an array element directly, Vue won't be able to detect the changes due to JavaScript limitations. You should be able to do it using the corresponding array method:
this.showDetailsDiv.splice(divNumber, 1, true);
You can find more ways of doing it here: https://v2.vuejs.org/v2/guide/list.html#Mutation-Methods
Also, if showDetailsDiv is a prop, you should probably avoid modifying it and create a copy in the component data instead.

Related

Style conflicts caused by using v-if and custom directives at the same time

Version
2.5.2
Reproduction link
codesandbox.io
Steps to reproduce
click the white button
v-wide will change the backgroundColor
What is expected?
The second popup's backgroundColor should be blue
What is actually happening?
The popup's backgroundColor is red that is the previous one
I use v-if and a custom directives to update the inline style at the same time. when the next element is displayed, it will inherit the style of the previous one.And the displayed element doesn't trigger the inserted lifecycle.
some code:
v-wide
const wide = Vue.directive("wide", {
inserted: function (e, binding, vnode) {
const style = window.getComputedStyle(e);
console.log(style.backgroundColor);
if (style.backgroundColor == "rgb(0, 0, 0)") {
e.style.backgroundColor = "red";
}
}
});
app.vue
<div class="popup popup1" v-if="showPopup1" v-wide>
<div class="btn-container">
<div class="btn" #click="showPopup1 = false;showPopup2 = true;"></div>
</div>
</div>
<div class="popup popup2" v-if="showPopup2" v-wide>
<div class="btn-container">
<div class="btn" #click="showPopup2 = false"></div>
</div>
</div>

Vue: Adding class to one item in v-for

I have a list of items in a v-for loop. I have a function on #click that will delete the item but I want to give it a class when I click it to change the background color for a short time so the user knows they clicked that item and it's deleting. In my deleteItem function, I set deletingItem to true but that obviously creates an issue because it will apply that class to all the divs in the in v-for. What is the best way to solve it so that it only gets applied to the div I click on?
<div :class="{'box': true, 'deleting-item': deletingItem}" v-for="(item,index) in items">
<div #click="deleteItem(item,index)>Delete</div>
</div>
You need to save the clicked item in a data property
<template>
<div>
<div v-for="(item, index) in items">
<button #click="deleteItem(index)" :class="{'delete-in-progress-class': index === indexOfDeletedItem}> {{item}} </button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: ['a', 'b', 'c']
indexOfDeletedItem: null
}
},
methods: {
deleteItem(index) {
this.indexOfDeletedItem = index;
setTimeout(() => { this.items.splice(index, 1); this.indexOfDeletedItem = null }, 1000); //delete item after 1s
}
}
}
</script>
<style>
.delete-in-progress-class {
background: red;
}
</style>
Obviously the solution above is naive. It'll probably go crazy if the user wants to delete many items in a short time, since the indices of an array shift around when you delete something.
But hopefully it'll give you an idea of how to conditionally apply styles in a list.

Vue event on element destruction

Is there a vue event when a v-if condition ceases to be true?
For example if I want to set y when div id foo is removed for whatever reason.
What should go in place of #whenDivGoes?
<div v-if="z">
<div id="foo"
v-if="x"
#whenDivGoes="y = true"
>
Hello
</div>
<div>
<div v-if="!x">Not X</div>
<div v-if="y">Bye</div>
If I understand correctly you want to set value of 'y' to be true when the foo div cease to exist.
Your foo div is shown when your 'x' is true, and hidden when x is false, so you can declare 'y' as a computed property that depends on x.
computed:{
y(){
return !x;
}
}
then
<div v-if="z">
<div id="foo"
v-if="x">
Hello
</div>
<div>
<div v-if="!x">Not X</div>
<div v-if="y">Bye</div>
note: declaring 'y' as computed property would prevent you from modifying/assigning value to it directly. if you want to 'do something' when 'y' value is changed you can use watch on 'y'.
With v-if, is the node is taken directly from the virtual DOM, you would need to watch and create your own callback.
If you are looking for a quick solution similar to your example, i have written a quick directive to you put directly into the element you want a callback on, but it would require you to use v-show as apposed to v-if (as it only changes the css display class)
Simply change the div you want a callback on when its not longer displayed, and add v-show-event:
<div id="foo"
v-show="x"
v-show-event="() => y = true"
>
So basically what's happening, is the directive checks if the element has style.display set to none
Vue.directive('show-event', {
update: function(el, binding, vnode) {
if(el.style.display === "none") {
if(typeof binding.value==='function') {
binding.value.bind(vnode.context)(event)
}
}
}
});
new Vue({
el: "#app",
data: {
done: false,
newEv: "this is some text"
},
methods: {
toggle: function(){
this.done = !this.done
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2
v-show="!done"
v-show-event="() => newEv = 'show event was hidden'">
v-show-event directive
</h2>
<button #click="toggle()">
Toggle
</button>
<div v-text="newEv"></div>
</div>

Vue input event not capturing entire field

I have a vue component that adds a search bar and search bar functionality. It contains this line:
<input class="input" type="text" placeholder="Address" v-model="searchQuery" v-on:input="(event) => this.$emit('queryChange', event)">
This captures the text in the search bar and emits it.
In my vue, this triggers my updateSearchQuery function:
this.searchQuery = event.data which merely saves the users input in the searchQuery property in my vue. Everything works fine when I do this, until, I make a search and then, make another call using the same this.searchQuery data.
For example, I'm trying to filter results with the search query '956'. I enter it and this call is made: GET /users?cp=1&pp=20&se=956, just like it should. Then after the page loads, if I go to page 2 of the results, this is the call that is made to the server: GET /users?cp=2&pp=20&se=6. Instead of saving 956 as the queryStr in the the view, it only saves the most recent character entered, instead of the entire content of the serch text.
This happens every time I type in multiple characters as a search query, and then make another call to the server using the unchanged this.searchQuery variable. If my initial search query is only a single character, it works just fine.
What am I doing wrong here? How can I emit the entirety of the text in the search bar, after any change, so that I can always save the whole search query, instead of the just the most recent change?
EDIT: I've add some more code below so the data flow is easier to follow:
Here is the template and script for the search component:
<template>
<div class="level-item">
<div class="field has-addons">
<div class="control">
<input class="input" type="text" placeholder="Address" v-model.lazy="searchQuery" v-on:input="(event) => this.$emit('queryChange', event)">
</div>
<div class="control">
<div class="button is-light" #click="clearInput">
<span class="icon is-small">
<i class="fa fa-times" style="color:#ffaaaa"></i>
</span>
</div>
</div>
<div class="control">
<button class="button is-info" #click="onSearch(searchQuery)">Search</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Search',
props: {onSearch: Function},
data () {
return {
searchQuery: ''
}
},
watch: {},
methods: {
clearInput () {
this.searchQuery = ''
}
}
}
</script>
the emitted queryChange event is caught and listened to in the vue page:
<Search :onSearch="onSearch" v-on:queryChange="updateSearchQuery"> and this triggers the updateSearchQuery function:
updateSearchQuery (event) {
this.searchQuery = event.data
console.log(event.data + ' || event.data')
console.log(this.searchQuery + ' || this.searchQuery')
}
Theoretically, the searchQuery data in my vue should be a copy of the searchQuery data in my component, which is itself merely a copy of whatever the user has input in the search bar.
Then when I make a call to the server I'm using the value in this.searchQuery in my vue:
onSearch (search) {
this.makeServerQuery(1, search)
},
onPaginate (page) {
this.makeServerQuery(page, this.searchQuery)
},
makeServerQuery (page = null, search = null) {
let queryStr = ''
if (page !== null) {
queryStr += '?cp=' + page + '&pp=' + this.perPage
}
if (this.searchQuery !== '') {
queryStr += '&se=' + this.searchQuery
} .....
The on onSearch(search) function is called whenever the search button is pressed. That seems to work fine, because when the button is pressed the entire searchQuery is passed, not just the last change.
An input event's data value appears to be the last typed character, and not the current value of the input. A simple fix is:
#input="$emit('queryChange', searchQuery)"
This works because the model will always be updated before the input event handler runs.
Here's a complete working component example:
<input
v-model="searchQuery"
type="text"
placeholder="Address"
#input="onInput"
/>
export default {
data() {
return { searchQuery: '' };
},
methods: {
onInput() {
console.log(this.searchQuery);
this.$emit('queryChange', this.searchQuery);
},
},
};

Conditionally rendering icons only one time depending on property

I am trying to write a template that displays either Icon AAA or Icon BBB, depending on whether or not the item in the current iteration has a specific flag. Here is my code:
<div v-for="(item, itemIndex) in items">
<div v-if="item.hasUnreadComments">
<span>Display Icon AAA</span>
</div>
<div v-else>
<span>Display Icon BBB</span>
</div>
</div>
The issue here is that I need either icon displayed ONCE. If more than one item has it set to item.hasUnreadComments === true, Icon AAA will be displayed equally as many times, which is not what I want. Couldnt find anything in the docs and I dont want to bind it to a v-model.
Can this be done in Vue without a third data variable used as a flag?
You will have to do some sort of intermediate transformation. v-if is just a flexible, low-level directive that will hide or show an element based on a condition. It won't be able to deal directly with how you expect the data to come out.
If I'm understanding what you're asking for, you want an icon to only be visible once ever in a list. You can prefilter and use an extra "else" condition. This sounds like a use case for computed properties. You define a function that can provide a transformed version of data when you need it.
This example could be improved upon by finding a way to boil down the nested if/elses but I think this covers your use case right now:
const app = new Vue({
el: "#app",
data() {
return {
items: [{
hasUnreadComments: true
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
]
}
},
computed: {
filteredItems() {
let firstIconSeen = false;
let secondIconSeen = false;
return this.items.map(item => {
if (item.hasUnreadComments) {
if (!firstIconSeen) {
item.firstA = true;
firstIconSeen = true;
}
} else {
if (!secondIconSeen) {
item.firstB = true;
secondIconSeen = true;
}
secondIconSeen = true;
}
return item;
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, itemIndex) in filteredItems">
<div v-if="item.firstA">
<span>Display Icon AAA</span>
</div>
<div v-else-if="item.firstB">
<span>Display Icon BBB</span>
</div>
<div v-else>
<span>Display no icon</span>
</div>
</div>
</div>