v-for causing actions to be applied to all divs - javascript

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.

Related

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

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.

Render component template on invoked methods

So while I'm learning vue, I wanted to double check if someone can show me what I'm doing wrong or lead me in the right answer. Below, I will show the code and then explain what I'm attempting to do.
Here is my Vue.js app:
Vue.component('o365_apps_notifications', {
template:
`
<div class="notification is-success is-light">
// Call the name here and if added/removed.
</div>
`,
});
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
components: 'o365_apps_notifications',
data() {
return {
list: {},
movable: true,
editable: true,
isDragging: false,
delayedDragging: false,
options: {
group: 'o365apps',
disabled: true,
handle: '.o365_app_handle',
}
}
},
methods: {
add(index, obj) {
console.log(obj.name);
this.$data.list.selected.push(...this.$data.list.available.splice(index, 1));
this.changed();
},
remove(index, obj) {
console.log(obj.name);
this.$data.list.available.push(...this.$data.list.selected.splice(index, 1));
this.changed();
},
checkMove(evt) {
console.log(evt.draggedContext.element.name);
},
},
});
Here is my modal:
<div id="o365-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Applications</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="o365-modal-edit-wrapper">
<div class="columns">
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<o365-apps-notifications></o365-apps-notifications>
</footer>
</div>
</div>
Here is what I'm attempting to do:
Inside my modal, I have my o365_apps_notifications html tag called, my add() and remove() methods output a name on each add/remove using console.log(obj.name); and my checkMove method also drags the same name on drag as shown below:
How could I get my component to render and output the name inside the modal footer? I've tried all methods, but I can't seem to figure out how to trigger the component.
Also, would I have to do something special to make the component fade out after a set timeframe?
All help is appreciated!
A couple issues:
You've declared the notification component with underscores (o365_apps_notifications), but used hyphens in the modal's template. They should be consistent (the convention is hyphens).
The notification component is declared globally (with Vue.component), but it looks like you're trying to add it to the modal's components, which is intended for local components. Only one registration is needed (the global component registration should do).
<o365-apps-notifications>
The notification component should have public props that take the item name and state:
Vue.component('o365-apps-notifications', {
props: {
item: String,
isAdded: Boolean
},
})
Then, its template could use data binding to display these props.
Vue.component('o365-apps-notifications', {
template:
`<div>
{{ item }} {{ isAdded ? 'added' : 'removed '}}
</div>`
})
For the fade transition, we want to conditionally render this data based on a local Boolean data property (e.g., named show):
Vue.component('o365-apps-notifications', {
template:
`<div v-if="show">
...
</div>`,
data() {
return {
show: false
}
}
})
...and add the <transition> element along with CSS to style the fade:
Vue.component('o365-apps-notifications', {
template:
`<transition name="fade">
<div v-if="show">
...
</div>
</transition>`,
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
To automatically fade out the data, add a watch on item, which sets show=true and then show=false after a delay:
Vue.component('o365-apps-notifications', {
watch: {
item(item) {
if (!item) {
return;
}
this.show = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.show = false, 1000);
}
}
})
Usage
In the modal component, declare local data properties that hold the currently added/removed item:
new Vue({
el: '#o365-modal-edit',
data() {
return {
changedItem: null,
changedItemIsAdded: false,
}
},
})
Also update add() and remove() to set these properties:
new Vue({
methods: {
add(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = true;
},
remove(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = false;
},
},
})
Then in the modal component's template, bind these properties to the notification component's props:
<o365-apps-notifications :item="changedItem" :is-added="changedItemIsAdded"></o365-apps-notifications>
demo

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: Dynamic Class Binding

Vue -V -- 3.0.5.
I have a component Cube.vue in which I'm trying to set a blue or green class to a child element dynamically.
I've created the component and have it imported into a specific page but I can't get the or to work correctly.
<template>
<div :class="$style.cubeInner">
<div class="cube" :class="{ 'cube--blue': isBlue ? 'cube--green': isGreen }">
<div v-for="side in cubeside" :class="side.class" :key="side.id"></div>
</div>
</figure>
</template>
And here is my export
export default {
data() {
return {
Cube: 'cube',
isBlue: Boolean,
isGreen: Boolean,
};
}
};
I import into another component and render it via <cube-hover></cube-hover>. My question is do I need to set a prop or a data() for isBlue to be true or false? I can't seem to target the child since the entire component is being imported.
Basically, I can't target that nested <div>, it just adds the class to the parent. And I want to add 'cube--blue' or 'cube--green' to specific pages.
Put the boolean into a data field, and then the condition check into a computed function.
...updated to add context
export default {
data: () => {
...
isBlue: Boolean,
isGreen: Boolean,
},
computed:
isBlue() {
if (is it blue?) return true;
return false;
},
isGreen() {
if (is it green?) return true;
return false;
}
}
<template>
...
<div class="cube" :class="{ isBlue ? 'cube--blue' : 'cube--green': isGreen }">
<!-- I think this is where you went wrong: "'cube--blue': isBlue ? 'cube--green': isGreen" see note -->
</template>
note
You have a "?" separating your classes which should either be a comma, or you are trying to do a ternary operation. Comma separation could possibly apply both at once and I suspect you don't want that. Or if you are trying to do conditional class assignment:
Fix your ternary syntax:
`condition ? value if true : value if false`
you are missing the
: value if false portion
What you probably want is:
`:class="isBlue ? 'cube--blue' : 'cube--green'"
Lastly
Now that I've written this out I sort of feel like I should recommend a different approach. Assuming that this cube is either green OR blue, but never both at the same time, you might want to combine the logic into a single step. Perhaps you want to use a conditional inside of a getColor function? This is particularly smart if you will ever have more than two colors. Then the fn just returns a color and you can interpolate that into your class name like:
<div :class="`cube--${color}`"></i>
I can't understand what do you mean by 'or'.
By looking at your data just type:
<div class="cube" :class="{ 'cube--blue': isBlue, 'cube--green': isGreen }">
Update:
Kraken meant to change you approach to:
<div class="cube" :class="`cube--${getColor}`">
and in your data just type:
data() {
return {
color: 'blue',
};
},
computed: {
getColor() {
return this.color;
},
},
With this approach you prepare yourself for maybe more colors in the future. By just updating this.color.
<li
v-for="item in items"
:key="item.id"
class="nav-item"
:class="{ dropdown: hasChildren(item.children) }"
>
methods: {
hasChildren(item) {
return item.length > 0 ? true : false;
},
}
I think this is the best way to solve this problem.
<div class="checkbox-wrapper">
<div :class="[isInsurancePictureRequired === 'yes' ? 'custom-checkbox-active' : '', 'custom-checkbox']">
<label class="pic-required-checkbox-label" for="yes">
<input type="radio" id="yes" name="picture-require" value="yes" #click="handleCheckBox" checked>
<span class="checkmark"></span>
Yes
</label>
</div>

Vue.js how to delete component v-for values

I am learning Vue, so I created radio button component, but I am struggling with how can one delete these values. My current solution deletes actual values, but selection is still displayed.
This is the component
<template id="fradio">
<div>
<div class="field is-horizontal">
<div class="field-label" v-bind:class = "{ 'required' : required }">
<label
class = "label"
>{{label}}
</label>
</div>
<div class="field-body">
<div>
<div class="field is-narrow">
<p class="control" v-for="val in values">
<label class = "radio">
<input
type="radio"
v-bind:name = "name"
v-bind:id = "name"
#click = "updateValue(val)"
>
<span>{{val[valueLabel]}}</span>
<span v-if="!valueLabel">{{val}}</span>
</label>
<label class="radio">
<button class="delete is-small" #click="removeValue"></button>
</label>
<slot></slot>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true,
},
inputclass: {
type: String,
},
required: {
type: Boolean,
default: false,
},
valueLabel:{
type: String,
},
returnValue:{
type: String,
},
values:{},
name:'',
},
data() {
return {
};
},
methods: {
updateValue: function (value) {
var selectedValue;
(!this.returnValue) ? selectedValue = value : selectedValue = value[this.returnValue];
this.$emit('input', selectedValue)
},
removeValue: function() {
this.$emit('input',null);
},
},
}
</script>
It should be easy, but I need someone to point out the obvious...
Update:
I just realized that you may be more focused on the data not dynamically updating, which means that your issue might be that the data in the parent component is not being updated. Most of your data is being passed down as props, so I'd need to see how the event is being fired in the parent component in order to help diagnose what's wrong. Based on the code you provided, it looks like your removeValue() function is emitting an event but I don't see any code that actually removes the value.
I would check the parent component to make sure that it is removing the child component and that should fix your problem!
Initial Answer:
Generally, when removing an item from a v-for list, you need to know the index of the item and use the Array.splice in order to modify the list to remove it.
Here's a generic example off the top of my head.
<template>
<ul>
<li v-for="(fruit, index) in fruits"
#click="removeItem(index)">
{{ fruit }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
fruits: ['Apple', 'Banana', 'Clementines']
}
},
methods: {
removeItem(index) {
this.fruits.splice(index, 1)
}
}
}
</script>
Let me know if you have any questions!

Categories

Resources