Vue.js toggle class on click with v-for - javascript

How do you toggle a class in vue.js for list rendered elements? This question is an extension on this well answered question. I want to be able to toggle each element individually as well as toggle them all. I have attempted
a solution with the below code but it feels fragile and doesn't seem to work.
A different solution would be to use a single variable to toggle all elements and then each element has a local variable that can be toggled on and off but no idea how to implement that..
// html element
<button v-on:click="toggleAll"></button>
<div v-for="(item, i) in dynamicItems" :key=i
v-bind:class="{ active: showItem }"
v-on:click="showItem[i] = !showItem[i]">
</div>
//in vue.js app
//dynamicItems and showItem will be populated based on API response
data: {
dynamicItems: [],
showItem: boolean[] = [],
showAll: boolean = false;
},
methods: {
toggleAll(){
this.showAll = !this.showAll;
this.showItem.forEach(item => item = this.showAll);
}
}

Here is the small example to acheive you want. This is just a alternative not exact copy of your code.
var app = new Vue({
el:'#app',
data: {
dynamicItems: [
{id:1,name:'Niklesh',selected:false},
{id:2,name:'Raut',selected:false}
],
selectedAll:false,
},
methods: {
toggleAll(){
for(let i in this.dynamicItems){
this.dynamicItems[i].selected = this.selectedAll;
}
}
}
});
.active{
color:blue;
font-size:20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.9/vue.js"></script>
<div id="app">
<template>
<input type="checkbox" v-model="selectedAll" #change="toggleAll"> Toggle All
<div v-for="(item, i) in dynamicItems">
<div :class='{active:item.selected}'><input type="checkbox" v-model="item.selected">Id : {{item.id}}, Name: {{item.name}}</div>
</div>
{{dynamicItems}}
</template>
</div>

I think all you need to do is this
v-bind:class="{ active: showItem || showAll }"
and remove the last line from toggleAll
You also need to use Vue.set when updating array values, as array elements aren't reactive.

Related

How to access $children in Vue 3 for creating a Tabs component?

I'm trying to create a Tabs component in Vue 3 similar to this question here.
<tabs>
<tab title="one">content</tab>
<tab title="two" v-if="show">content</tab> <!-- this fails -->
<tab :title="t" v-for="t in ['three', 'four']">{{t}}</tab> <!-- also fails -->
<tab title="five">content</tab>
</tabs>
Unfortunately the proposed solution does not work when the Tabs inside are dynamic, i.e. if there is a v-if on the Tab or when the Tabs are rendered using a v-for loop - it fails.
I've created a Codesandbox for it here because it contains .vue files:
https://codesandbox.io/s/sleepy-mountain-wg0bi?file=%2Fsrc%2FApp.vue
I've tried using onBeforeUpdate like onBeforeMount, but that does not work either. Actually, it does insert new tabs, but the order of tabs is changed.
The biggest hurdle seems to be that there seems to be no way to get/set child data from parent in Vue 3. (like $children in Vue 2.x). Someone suggested to use this.$.subtree.children but then it was strongly advised against (and didn't help me anyway I tried).
Can anyone tell me how to make the Tab inside Tabs reactive and update on v-if, etc?
This looks like a problem with using the item index as the v-for loop's key.
The first issue is you've applied v-for's key on a child element when it should be on the parent (on the <li> in this case).
<li v-for="(tab, i) in tabs">
<a :key="i"> ❌
</a>
</li>
Also, if the v-for backing array can have its items rearranged (or middle items removed), don't use the item index as the key because the index wouldn't provide a consistently unique value. For instance, if item 2 of 3 were removed from the list, the third item would be shifted up into index 1, taking on the key that was previously used by the removed item. Since no keys in the list have changed, Vue reuses the existing virtual DOM nodes as an optimization, and no rerendering occurs.
A good key to select in your case is the tab's title value, as that is always unique per tab in your example. Here's your new Tab.vue with the index replaced with a title prop:
// Tab.vue
export default {
props: ["title"], πŸ‘ˆ
setup(props) {
const isActive = ref(false)
const tabs = inject("TabsProvider")
watch(
() => tabs.selectedIndex,
() => {
isActive.value = props.title === tabs.selectedIndex
} πŸ‘†
)
onBeforeMount(() => {
isActive.value = props.title === tabs.selectedIndex
}) πŸ‘†
return { isActive }
},
}
Then, update your Tabs.vue template to use the tab's title instead of i:
<li class="nav-item" v-for="tab in tabs" :key="tab.props.title">
<a πŸ‘†
#click.prevent="selectedIndex = tab.props.title"
class="nav-link" πŸ‘†
:class="tab.props.title === selectedIndex && 'active'"
href="#" πŸ‘†
>
{{ tab.props.title }}
</a>
</li>
demo
This solution was posted by #anteriovieira in Vuejs forum and looks like the correct way to do it. The missing piece of puzzle was getCurrentInstance available during setup
The full working code can be found here:
https://codesandbox.io/s/vue-3-tabs-ob1it
I'm adding it here for reference of anyone coming here from Google looking for the same.
Since access to slots is available as $slots in the template (see Vue documentation), you could also do the following:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>

Are Vue 3 class bindings with variables required to be in-line?

Given the following HTML:
<template v-for="(child, index) in group">
<div :class="{'border-pink-700 bg-gray-100 ': selected === child.id}">
<div>Container Content</div>
</div>
</template>
Is there a way to move the class binding out of the HTML, given that it relies on a condition passed via the v-for loop (child.id)?
The docs mention being able to bind computed properties, but my understanding is that these don't accept arguments (and I haven't been able to get it to work that way).
You can use a method and pass the item to the method:
<div :class="classes(child)">
setup() {
...
const classes = (child) => {
return {
'border-pink-700 bg-gray-100': selected.value === child.id
}
}
return {
...
selected,
classes
}
}
If you were using Vue 2 or the Options API:
methods: {
classes(child) {
return {
'border-pink-700 bg-gray-100': this.selected === child.id
}
}
}
Be sure to avoid changing instance properties in the method, but reading is ok.

Vue.js - Wrong Checkbox is checked

With Vue.js, I'm showing the list of items with a checkbox. Clicking on Checkbox will move the item down with strike-through. The issue is, when I click on the checkbox, the wrong checkbox is checked.
For eg, when I click on Apple checkbox, orange checkbox is checked.
Fiddle: https://jsfiddle.net/d6encxe1/
Here's my code,
var myApp = new Vue({
el: '#myApp',
data: {
lists: [
{title: 'Apple', isChecked: false},
{title: 'Orange', isChecked: false},
{title: 'Grapes', isChecked: false}
]
},
computed: {
filterLists: function(){
return _.orderBy(this.lists, ['isChecked', false]);
}
},
methods: {
completeTask: function(e, i){
e.preventDefault();
this.lists[i].isChecked = !this.lists[i].isChecked;
}
}
})
.completed{
text-decoration: line-through;
color: red;
}
<script src="https://unpkg.com/vue"></script>
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
<div id="myApp">
<ul>
<li v-for="(list, index) in filterLists">
<input type="checkbox" v-bind:id="'todo-' + index" v-on:change="completeTask($event, index)" />
<span class="title" v-bind:class="{completed: list.isChecked}">{{list.title}}</span>
</li>
</ul>
</div>
Use a key. A key uniquely identifies an element in Vue. If you do not use a key, Vue will try to re-use existing elements for performance.
To give Vue a hint so that it can track each node’s identity, and thus
reuse and reorder existing elements, you need to provide a unique key
attribute for each item. An ideal value for key would be the unique id
of each item.
You should always use a key when rendering list. Here I'm using the title of your list items, but ideally you should generate a unique key.
<li v-for="(list, index) in filterLists" :key="list.title">
Also you do not need to pass indexes around. Just pass the item itself.
v-on:change="completeTask(list)"
And in completeTask, check it off.
completeTask: function(task){
task.isChecked = !task.isChecked
}
Finally, iterate over your li element and not your ul element.
Updated fiddle.
The index screws things up, you can fix it by binding the checked state
<input type="checkbox" v-bind:checked="list.isChecked" v-bind:id="'todo-' + index" v-on:change="completeTask($event, index)" />
And changing the complete task to just pass the item in:
<input type="checkbox" v-bind:id="'todo-' + index" v-on:change="completeTask(list)" v-bind:checked="list.isChecked" />
See fiddle:
https://jsfiddle.net/d6encxe1/3/
You should remove the 'id' or use something other than the index of the loop because when you re-order the index doesn't change.
You probably want to put v-for in <li> instead of <ul>, or your will get several <ul> elements.
And you didn't provide a key. You should provide a unique key for the items. For example, if the title is unique, you can use it as the key, or you may need to add another attribute like id.
Besides, you can pass the entire list item to the method instead of just the index, because the indexes are changeable in your case:
v-on:change="completeTask($event, list)"
Working example here: https://jsfiddle.net/0r2yb0z6/1/

How do I target all items in a list, when a change occurs in Vue.js?

I'm building a site that uses Vue for to power the majority of the UI. The main component is a list of videos that is updated whenever a certain URL pattern is matched.
The main (video-list) component looks largely like this:
let VideoList = Vue.component( 'video-list', {
data: () => ({ singlePost: '' }),
props: ['posts', 'categorySlug'],
template: `
<div>
<transition-group tag="ul">
<li v-for="(post, index) in filterPostsByCategory( posts )">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>
</transition-group>
</div>`,
methods: {
orderPostsInCategory: function ( inputArray, currentCategory ) {
let outputArray = [];
for (let i = 0; i < inputArray.length; i++) {
let currentCategoryObj = inputArray[i].video_categories.find( (category) => {
return category.slug === currentCategory;
});
let positionInCategory = currentCategoryObj.category_post_order;
outputArray[positionInCategory] = inputArray[i];
}
return outputArray;
},
filterPostsByCategory: function ( posts ) {
let categorySlug = this.categorySlug,
filteredPosts = posts.filter( (post) => {
return post.video_categories.some( (category) => {
return category.slug === categorySlug;
})
});
return this.orderPostsInCategory( filteredPosts, categorySlug );
}
}
});
The filterPostsByCategory() method does its job switching between the various possible categories, and instantly updating the list, according to the routes below:
let router = new VueRouter({
mode: 'history',
linkActiveClass: 'active',
routes: [
{ path: '/', component: VideoList, props: {categorySlug: 'home-page'} },
{ path: '/category/:categorySlug', component: VideoList, props: true }
]
});
The difficulty I'm having is transitioning the list in the way that I'd like. Ideally, when new category is selected all currently visible list items would fade out and the new list items would then fade in. I've looked at the vue transitions documentation, but haven't been able to get the effect I'm after.
The issue is that some items have more than one category, and when switching between these categories, those items are never affected by whatever transition I try to apply (I assume because Vue is just trying to be efficient and update as few nodes as possible). It's also possible that two or more categories contain the exact same list items, and in these instances enter and leave methods don't seem to fire at all.
So the question is, what would be a good way to ensure that I can target all current items (regardless of whether they're still be visible after the route change) whenever the route patterns above are matched?
Have you noticed the special key attribute in the documentation?
Vue.js is really focused on performance, because of that, when you modify lists used with v-for, vue tries to update as few DOM nodes as possible. Sometimes it only updates text content of the nodes instead of removing the whole node and then append a newly created one. Using :key you tell vue that this node is specifically related to the given key/id, and you force vue to completely update the DOM when the list/array is modified and as a result the key is changed. In your case is appropriate to bind the key attribute to some info related to the post and the category filter itself, so that whenever the list is modified or the category is changed the whole list may be rerendered and thus apply the animation on all items:
<li v-for="(post, index) in filterPostsByCategory( posts )" :key="post.id + categorySlug">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>

The content not shown properly in function callback in Vue.js

I've got two problems here. The first is that I can't get the star rendered properly. I can do it if I change the value in the data() function but if I want to do it in a function callback way, it doesn't work (see comments below). What's going wrong here? Does it have something to do with Vue's lifecycle?
The second one is that I want to submit the star-rate and the content of the textarea and when I refresh the page, the content should be rendered on the page and replace the <textarea></textarea> what can I do?
I want to make a JSFiddle here but I don't know how to make it in Vue's single-file component, really appreciate your help.
<div class="order-comment">
<ul class="list-wrap">
<li>
<span class="comment-label">rateA</span>
<star-rating :data="dimensionA"></star-rating>
</li>
</ul>
<div>
<h4 class="title">comment</h4>
<textarea class="content" v-model="content">
</textarea>
</div>
<mt-button type="primary" class="mt-button">submit</mt-button>
</div>
</template>
<script>
import starRating from 'components/starRating'
import dataService from 'services/dataService'
export default {
data () {
return {
dimensionA: '' //if I changed the value here the star rendered just fine.
}
},
components: {
starRating
},
methods: {
getComment (id) {
return dataService.getOrderCommentList(id).then(data => {
this.dimensionA = 1
})
}
},
created () {
this.getComment(1) // not working
}
}
</script>
What it seems is scope of this is not correct in your getComment method, you need changes like following:
methods: {
getComment (id) {
var self = this;
dataService.getOrderCommentList(id).then(data => {
self.dimensionA = 1
})
}
},
As you want to replace the <textarea> and render the content if present, you can use v-if for this, if content if available- show content else show <textarea>
<div>
<h4 class="title">comment</h4>
<span v-if="content> {{content}} </span>
<textarea v-else class="content" v-model="content">
</textarea>
</div>
See working fiddle here.
one more problem I have observed in your code is you are using dynamic props, but you have assigned the prop initially to the data variable value in star-rating component, but you are not checking future changes in the prop. One way to solve this, assuming you have some other usage of value variable is putting following watch:
watch:{
data: function(newVal){
this.value = newVal
}
}
see updated fiddle.

Categories