Vuejs - add different classes in elements generated with v-for - javascript

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.

Related

Vue3 can't get dynamic styling to only apply to specific buttons

I am in the process of learning vue and I'm stumped on how to get these buttons to dynamically style separately when clicked. These are filters for a list of products and I would like the apply one style when the filter is 'on' and a different style when the filter is 'off'. I can get the styles to update dynamically, but all of the buttons change style when any of them are clicked. The actual filter functionality is working as expected (the products are being filtered out when the button for that product is clicked).
In the code snippet, mode is passed to the BaseButton component, which is then applied as the class.
<template>
<ul>
<li v-for="genus of genusList" :key="genus.label">
<BaseButton #click="filterGenus(genus.label)" :mode="genusClicked.clicked ? 'outline' :''">
{{ genus.label }}
</BaseButton>
</li>
<BaseButton #click="clearFilter()" mode="flat">Clear Filter</BaseButton>
</ul>
</template>
methods: {
filterGenus(selectedGenus) {
this.clickedGenus = selectedGenus
this.clicked = !this.clicked
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.$emit('clear-filter')
}
},
I have tried making a computed value to add a .clicked value to the genusList object but that didn't seem to help.
Maybe something like following snippet (if you need more buttons to be styled at once save selected in array, if only one just save selected):
const app = Vue.createApp({
data() {
return {
genusList: [{label: 1}, {label: 2}, {label: 3}],
selGenus: [],
};
},
methods: {
isSelected(selectedGenus) {
return this.selGenus.includes(selectedGenus)
},
filterGenus(selectedGenus) {
if (this.isSelected(selectedGenus)) {
this.selGenus = this.selGenus.filter(s => s !== selectedGenus)
} else {
this.selGenus = [...this.selGenus, selectedGenus]
}
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.selGenus = []
this.$emit('clear-filter')
}
},
})
app.component('baseButton', {
template: `<button :class="mode"><slot /></button>`,
props: ['mode']
})
app.mount('#demo')
.outline {
outline: 2px solid red;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<ul>
<li v-for="genus of genusList" :key="genus.label">
<base-button #click="filterGenus(genus.label)"
:mode="isSelected(genus.label) ? 'outline' :''">
{{ genus.label }}
</base-button>
</li>
<base-button #click="clearFilter()" mode="flat">
Clear Filter
</base-button>
</ul>
</div>

check props value in child component if available

I'm working currently with BootstrapVue.
I have a b-dropdown in my parent.vue where I can select a object of a JSON-File and convert it into an array because I need the length of this json object. This works fine!!
My problem is that I need to check in my parent.vue if something was selected - so if this.arrayLength is higher than 0 (until this point it works all well!). If this is true, it should use and show addElementsNotClickable() in my child.vue where no elements can be added (count of the inputs are equal to length of array) - otherwise it should use and show my button addElement() where multiple elements can be added manually.
But I'm not able to check in my child.vue if arrayLenght > 0... AND i don't know what to use on second button e.g #change(??) How can I solve that?
Many thanks! I've tried to be as detailed as I can!
Additional Info: I get no error codes!!
my parent.vue:
methods: {
inputedValue(input, index) {
var array = [];
const item= this.json.find((i) => i.Number === input);
for (let key in item.ID) {
array.push(item.ID[key]);
}
if(array.length > 0) {
this.getIndex = index;
this.getDataArray = array;
this.getLengthArray = array.length;
}
}
}
my child.vue (template)
<div class="mt-4 mb-5 ml-3 mr-3">
<b-button v-if="!hide" #click="addElement" variant="block">Add Element</b-button>
<b-button v-if="hide" #???="addElementNotClickable" variant="block">Not clickable ! </b-button>
</div>
my child.vue (script)
methods: {
addElementsNotClickable() {
for(let i = 1; i < this.arrayLength; i++) {
this.inputs.push({})
}
},
addElement() {
this.inputs.push({})
},
}
data() {
return {
inputs: [{}]
arrayLength: this.getLengthArray,
arrayIndex: this.getIndex,
hide: false,
}
props: [
"getLengthArray",
"getIndex"
],
You are misunderstanding how components should work in Vue. In short you can understand them by:
parent send props down and child send events up
What you are looking for is that whenever your arrayLength updates, you send an event to the parent. Then, it is the responsibility of the parent to handle that event. In this case, the parent would receive the event and store the length of the array.
Parent.vue
<template>
<div>
<child #arrayLenght:update="onArrayLenghtUpdate"/>
</div>
</template>
<script>
export default {
data: () => {
arrayLength: 0,
},
methods: {
onArrayLenghtUpdate(length) {
this.arrayLength = length;
}
}
}
</script>
Child.vue
<template> ... </template>
<script>
export default {
data: () => ({
arrayLength: 0,
}),
watch: {
arrayLenghth: function (newLenght) {
this.$emit('arrayLenght:update', newLenght);
}
}
}
</script>
This is the standard way and extremely useful if your Parent and Child aren't highly coupled together. If they are dependent on each other (you won't use Child.vue anywhere else in the app. Just as direct child of Parent.vue) then you can use the inject/provide approach. This approach is beyond the scope of this answer, feel free to read an article on it

show element in v-for list: VueJS [duplicate]

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)">

Vuejs: method with v-if not working

I think this might be a typo somewhere, but can't find the issue. I have this Vuejs template, that renders just fine if I remove the v-if verification. However when I use it, it does not render anything at all. Already placed a debugger both in return true, and return false, and the logic test returns true only once, as expected. Can anybody spot what am I doing wrong?
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<collapsible-cards
v-for="tipo, index in tiposCollapsibles"
v-if="mostraApenasPerfilEspecificado(perfil, tipo)"
v-bind:key=index
v-bind:dados="tipo"
>
</collapsible-cards>
</ul>
</div>`,
mounted: function() {
for (key in this.tiposCollapsibles) {
if (this.tiposCollapsibles[key].perfisQuePodemVer.indexOf(this.perfil) >= 0) {
this.queryTeleconsultorias(key);
}
}
},
methods: {
mostraApenasPerfilEspecificado(perfil, tipo) {
tipo['perfisQuePodemVer'].forEach(function(value) {
if (perfil === value) {
return true;
}
});
return false;
},
...
Update: For anyone who is having the same problem, I ended up using a computed property, rather than a method itself. The v-if/-v-show behaviour to show/hide elements was moved to the computed property. In the end I was not sure if this was an issue with Vuejs. Here is the working code:
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<collapsible-cards
v-if="showTipoCollapsibles[index]"
v-for="tipo, index in tiposCollapsibles"
v-bind:key="index"
v-bind:object="tipo"
>
</collapsible-cards>
</ul>
</div>`,
mounted: function() {
this.executeQuery(this.perfil);
},
computed: {
showTipoCollapsibles: function() {
let perfisVisiveis = {};
for (tipo in this.tiposCollapsibles) {
perfisVisiveis[tipo] = this.tiposCollapsibles[tipo].enabledForProfiles.includes(this.perfil);
}
return perfisVisiveis;
},
},
methods: {
executeQuery: function(value) {
if (value === 'monitor') {
var query = gql`query {
entrada(user: "${this.user}") {
id
chamadaOriginal {
datahoraAbertura
solicitante {
usuario {
nome
}
}
}
...
Change from v-if to v-show
v-show="mostraApenasPerfilEspecificado(perfil, tipo)"
You can also use template to use v-if outside child component as
template: `
<div class="workbench container">
<ul class="collapsible popout" data-collapsible="expandable">
<template v-for="(tipo, index) in tiposCollapsibles">
<collapsible-cards
v-if="mostraApenasPerfilEspecificado(perfil, tipo)"
v-bind:key="index"
v-bind:dados="tipo">
</collapsible-cards>
</template>
</ul>
</div>`,
If not work, share live demo
There appears to be a similar bug in Bootstrap-Vue, where v-if only works on non-Bootstrap elements.
With otherwise identical code, this element will not appear when this.error = true:
<b-alert v-if="error" variant="danger">Failed!</b-alert>
But this will:
<div v-if="error">Failed!</div>
Actually, you have to use computed rather than method.
computed: {
showTipoCollapsibles: function() {},
executeQuery: function(value) {},
},
methods: {}

v-for causing actions to be applied to all divs

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.

Categories

Resources