This question already has an answer here:
Vue.js - Add class to clicked button
(1 answer)
Closed 3 years ago.
I have a v-for which display all my items and I have a panel for each items (to modify and delete) but when I click on this button to display my panel, it appears on all of my items. How can I avoid that ? This is the same thing when I click on modify button, the input to modify my item appears on each element.
There is my code :
<div v-for="(comment, index) in comments" :list="index" :key="comment">
<div v-on:click="show = !show">
<div v-if="show">
<button #click="edit(comment), active = !active, inactive = !inactive">
Modify
</button>
<button #click="deleteComment(comment)">
Delete
</button>
</div>
</div>
<div>
<p :class="{ active: active }">
{{ comment.content }}
</p>
<input :class="{ inactive: inactive }" type="text" v-model="comment.content" #keyup.enter="doneEdit">
</div>
</div>
And the methods & data :
data() {
return {
show: false,
editing: null,
active: true,
inactive: true
}
},
methods: {
edit(comment) {
this.editing = comment
this.oldComment = comment.content
},
doneEdit() {
this.editing = null
this.active = true
this.inactive = true
}
}
You have the same show, editing, active, inactive state for all items. So if you change some data property for one item it changed for all.
There are a lot of ways to achieve what you want.
The easiest is to manage your data by index.
For example:
<div v-on:click="showIndex = index">
<div v-if="showIndex === index">
...
data () {
return {
showIndex: null
...
The main problem with this approach - you can show/edit only one item at the time.
If you need more complicated logic and whant to manage more then one item at the time I suggest to create a separate component for your items and each will have own state (show, editing etc.)
#NaN's approach works if you want to only have one open at a time. If you want to have the possibility of having multiple open at the same time you would need to keep track of each individual element. Right now you are only basing it on show. Which can only be true/false for all elements at the same time.
So this is what you need to do:
Change show from a boolean to an array
data() {
return {
show: [],
editing: null,
active: true,
inactive: true,
}
},
Then you can keep track of which element should have the panel or not:
<div v-on:click="toggleActive(index)">
And the method:
methods: {
toggleActive(index) {
if (this.show.includes(index)) {
this.show = this.show.filter(entry => entry !== index);
return;
}
this.show.push(index);
}
}
and finally your v-if becomes:
<div v-if="show.includes(index)">
Related
I have got a clickable list in a Vue/Nuxt application. When one item is selected, a little tick mark appears. I would like to be able to unselect an item (the tick mark to disappear) if the item is clicked again. If I click on another item, I would like this item to be selected and the previously selected item to unselect (only one item can be selected). So far, if I try to select another item, I need to click twice because the first click will only unselect the first selected item and the second click will select the new item. Any idea ??
<template>
<div
v-for="(item, itemIndex) in list"
:key="itemIndex"
#click="onClick(itemIndex)"
>
<div>
<div v-if="activeIndex == itemIndex && selected === true">
<TickMark />
</div>
<Item />
</div>
</div>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null,
selected: false,
}
},
methods: {
onClick (index) {
this.activeIndex = index
this.selected = !this.selected
},
},
}
</script>
because you don't need to change positions or sort the list - keeping the selected index is just fine, do it like this:
<template>
<section
class="items-list">
<template v-for="(item, itemIndex) in list"
:key="itemIndex" >
<TickMark v-if="activeIndex === itemIndex
#click="selectItem(itemIndex)" /> // by clicking on the mark - it will toggle the selection
<Item />
</template>
</section>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null
}
},
methods: {
selectItem (index) {
this.activeIndex = index
},
},
}
</script>
I've changed the architecture of the DOM so it will be without all the un-necessary elements
I need to add a class to a list group create using bootstrap 5 in my vuejs app. I know about class binding but in my case I'm not sure how to proceed. I want that when the user click on an item inside the list, the clicked item get the disabled active class and the other elements gets only the disabled class. At the moment I have this code in my template
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action" v-for="(choice, index) in item.choices" :key="index">
<small class="" #click.prevent="checkAnswer(item.questionIndex, index)">{{ index }}) {{ choice }}</small>
</li>
</ul>
The v-for loop will generate the elements and when an element is clicked a method is called to check the user choice. In my app script I have this code
export default {
name: 'Survey',
data() {
return {
n: 0,
answeredQuestions: [],
}
},
mounted() {
},
computed: {
questions() {
return this.$store.getters.survey;
},
},
methods: {
showNext() {
if( this.n < this.questions.length ){
this.n++
}
},
isAnswered(index) {
return this.n !== index ? 'hide' : '';
},
checkAnswer(questionIndex, choice) {
this.answeredQuestions.push(true);
this.showNext();
...
}
}
}
What's the best way to implement the needed class binding?
There's a lot of unknowns about the rest of your code (how the questions are handled and switched through, etc.), but here's a working example for a single question. So you'll have to adapt this for having multiple questions in your app, but it should push you in the right direction. I used an inline :style attribute in addition to the static styles already present on the <li>, but you could move that to a function as suggeted in Peter's answer, if you prefer.
const app = {
name: 'Survey',
data() {
return {
n: 0,
questions: [],
answeredQuestions: [],
item: {
questionIndex: 1,
choices: ['Lorem', 'Ipsum']
},
selectedChoice: null
}
},
mounted() {
},
computed: {
questions() {
return this.$store.getters.survey;
},
},
methods: {
showNext() {
if (this.n < this.questions.length) {
this.n++
}
},
isAnswered(index) {
return this.n !== index ? 'hide' : '';
},
checkAnswer(questionIndex, choice) {
this.answeredQuestions.push(choice);
this.showNext();
}
}
};
Vue.createApp(app).mount('#app');
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<script src="https://unpkg.com/vue#3.0.11/dist/vue.global.prod.js"></script>
<div id="app">
<ul class="list-group list-group-flush">
<li class="list-group-item list-group-item-action" :class="{disabled: answeredQuestions.length, active: answeredQuestions.includes(index)}" v-for="(choice, index) in item.choices" :key="index" #click.prevent="checkAnswer(item.questionIndex, index)">
<small class="">{{ index }}) {{ choice }}</small>
</li>
</ul>
</div>
If I understand correctly your situation and what you intend to do here I would suggest using the item in the checkAnswer method so that an identifier is used to set a computed property to the current item.questionIndex.
Then you bind the class of each element with a ternary operator condition to check the questionIndex and return the proper classes string: <small :class="questionIndex == item.questionIndex ? 'disabled active':'disabled'" ...
You search the internet for vue class binding and it's the first result that pops up:
https://v2.vuejs.org/v2/guide/class-and-style.html
You can use an plain object, object from your data, a function returning an object or simply a string. You can make any attribute dynamic with v-bind:, or simply :.
Your checkAnswer() function can cause a change in classes by manipulating something in data, for example.
See tutorial above for example code. Keep in mind v-bind:class is the same as :class.
The "best way" changes like every week in Vue, just find a way to do it and learn its advantages and disadvantages.
An example would be:
template: let a function generate the classes
<small
:class="getChoiceClasses(item, choice, index)"
#click.prevent="checkAnswer(item.questionIndex, index)"
>{{ index }}) {{ choice }}</small>
script: add method
getChoiceClasses(item, choice, index) {
let classes = {
active: choice == 1, // for example
disabled: false, // default
even: index % 2 == 0
};
if (whateverYouNeedToCheck) {
classes.disabled = true;
}
return classes;
}
A method is a little slower than a value from data, but it's very minor and only becomes a problem when you have 100s of calls.
How can i make dynamic switching between buttons? For example if one button is active, then the rest is not active. For example:
constructor(props) {
super(props)
this.state = {
active: false,
notactive: false
}
}
checkActive = () => {
this.setState({
active: true,
notactive: false
})
};
checkNotactive = () => {
this.setState({
active: false,
notactive: true
})
};
But i want make it dynamic.
I mean that when I have, for example, 10 buttons, I will not set each state apart. If I add another button, it will work. Just like the radio button.
Instead of a boolean you could use an index to mark the active button.
Eg:
this.state = {
activeButtonIndex: null
}
then when creating your buttons using a loop you can check
if (index === this.state.activeButtonIndex) { do something }
You would need to have a type property based on your buttons. If for example you need to make 3 buttons daily, weekly, monthly then you can have something like this.
return (
<div style={style.buttonContainer}>
<div className={this.props.active === "daily" ? "activeButton" : ""}>
<Button color="primary" onClick={this.handleClick("daily")}>
Daily
</Button>
</div>
<div className={this.props.active === "weekly" ? "activeButton" : ""}>
<Button color="primary" onClick={this.handleClick("weekly")}>
Weekly
</Button>
</div>
<div className={this.props.active === "monthly" ? "activeButton" : ""}>
<Button color="primary" onClick={this.handleClick("monthly")}>
Monthly
</Button>
</div>
</div>
);
I have a property on parent's state active and based on that I will switch dynamically.
I am trying to make a component with a list of items and when I click on each of the items, it shows me an edit popup. When I click on it again, it hides the edit popup. But I would like to also be able to click anywhere on the document and hide all edit popups (by setting edit_item_visible = false).
I tried v-on-clickaway but since I have a list of items then it would trigger multiple times. And the #click event would trigger first and then the clickaway event would trigger multiple times and hide it right after showing it. I also tried to change the component's data from outside but with no luck.
Vue.component('item-list', {
template: `
<div>
<div v-for="(item, index) in items" #click="showEdit(index)">
<div>{{ item.id }}</div>
<div>{{ item.description }}</div>
<div v-if="edit_item_visible" class="edit-item">
Edit this item here...
</div>
</div>
</div>
`,
data()
{
return {
items: [],
edit_item_visible: false,
selected: null,
};
},
methods:
{
showEdit(index)
{
this.selected = index;
this.edit_item_visible = !this.edit_item_visible;
}
},
});
const App = new Vue ({
el: '#app',
})
If you want to be able to edit multiple items at the same time, you should store the list of edited items, not global edit_item_visible flag.
showEdit(item)
{
this.selected = item;
this.editing_items.push(item);
}
// v-on-clickaway="cancelEdit(item)"
cancelEdit(item)
{
let idx = this.editing_items.indexOf(item);
this.editing_items.splice(idx, 1);
}
Previously I asked a question about removing a custom truncate filter in Vue. Please see the question here:
Removing a Vue custom filter on mouseover
However, I neglected to mention that I am using a v-for loop and when I hover over one div, I am noticing that all the divs in the loop are having the same action applied to them. I'm not sure how to target only the div that is being hovered over. Here is my template:
<div id="tiles">
<button class="tile" v-for="(word, index) in shuffled" #click="clickWord(word, index)" :title="word.english">
<div class="pinyin">{{ word.pinyin }}</div>
<div class="eng" #mouseover="showAll = true" #mouseout="showAll = false">
<div v-if="showAll">{{ word.english }}</div>
<div v-else>{{ word.english | truncate }}</div>
</div>
</button>
</div>
And the data being returned:
data(){
return {
currentIndex: 0,
roundClear: false,
clickedWord: '',
matchFirstTry: true,
showAll: false,
}
},
If you know Vue, I would be grateful for advice. Thanks!
In your example, showAll is being used for each of the buttons generated by the v-for to determine whether or not to show the complete text of the word.english value. This means that whenever the mouseover event of any the .eng class divs fires, the same showAll property is being set to true for every button.
I would replace the showAll Boolean value with a showWordIndex property initially set to null:
data() {
showWordIndex: null,
},
And then in the template, set showWordIndex to the index of the word on the mouseover handler (and to null in the mouseleave handler):
<button v-for="(word, index) in shuffled" :key="index">
<div class="pinyin">{{ word.pinyin }}</div>
<div
class="eng"
#mouseover="showWordIndex = index"
#mouseout="showWordIndex = null"
>
<div v-if="showWordIndex === index">{{ word.english }}</div>
<div v-else>{{ word.english | truncate }}</div>
</div>
</button>
Here's a working fiddle.
Even better would be to make a new component to encapsulate the functionality and template of everything being rendered in the v-for, passing the properties of each word object to the child component as props.
This way, you would still use the showAll property like you are in your example, but you would define it in the child component's scope. So now the showAll property will only affect the instance of the component it's related to.
Below is an example of that:
Vue.component('tile', {
template: '#tile',
props: ['pinyin', 'english'],
data() {
return { showAll: false };
},
filters: {
truncate: function(value) {
let length = 50;
if (value.length <= length) {
return value;
} else {
return value.substring(0, length) + '...';
}
}
},
})
new Vue({
el: '#app',
data() {
return {
words: [
{pinyin: 1, english: "really long string that will be cut off by the truncate function"},
{pinyin: 2, english: "really long string that will be cut off by the truncate function"},
{pinyin: 3, english: "really long string that will be cut off by the truncate function"},
{pinyin: 4, english: "really long string that will be cut off by the truncate function"},
],
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<div id="app">
<tile v-for="word, i in words" v-bind="word" :key="word"></tile>
</div>
<script id="tile" type="x-template">
<button :title="english">
<div class="pinyin">{{ pinyin }}</div>
<div class="eng" #mouseover="showAll = true" #mouseout="showAll = false">
<div v-if="showAll">{{ english }}</div>
<div v-else>{{ english | truncate }}</div>
</div>
</button>
</script>
In order to do this, you can't use a computed property (as I originally suggested in the answer of mine that you linked), since you need to be aware of the context that you are in. That said, you CAN use a filter if you apply a showAll property to each individual instance. If you declare this up front in your data model, the property will be reactive and you can toggle each item individually on mouseover and mouseout.
template:
<div id="app">
<div id="tiles">
<div class="tile" v-for="(word, index) in shuffled" :title="word.english">
<div class="pinyin">{{ word.pinyin }}</div>
<div class="eng" #mouseover="word.showAll = true" #mouseout="word.showAll = false">
{{ word.english | truncate(word) }}
</div>
</div>
</div>
</div>
js:
new Vue({
el: '#app',
data() {
return {
shuffled: [
{ english: 'here', showAll: false},
{ english: 'are', showAll: false },
{ english: 'there', showAll: false },
{ english: 'words', showAll: false }
],
currentIndex: 0,
roundClear: false,
clickedWord: '',
matchFirstTry: true,
}
},
filters: {
truncate: function(value, word) {
console.log(word)
let length = 3;
if (word.showAll || value.length <= length) return value;
return value.substring(0, length) + '...';
}
},
})
See working JSFiddle
The key is to apply showAll to each word instance and to then pass that word instance back to the filter so that we can check the value of the showAll property. As long as you declare it up front, Vue's reactivity system handles the rest for you.
Note that in this example it isn't necessary to use two elements with a v-if/else. A single element with a filter works perfectly.