I'm making a simple todo list app, and wondering on how to apply styles on only specific dynamic v-for elements.
<f7-list-item v-for="(listItem, index) in activeList.listItems" :key="index" :class="(checked) ? 'checked':'not-checked'">
{{ index+1 }}. {{ listItem }}
<span #click="checkTaskDone(index)">
<i class="f7-icons" id="check-task-btn">check_round</i>
</span>
</f7-list-item>
export default {
data() {
return {
checked: false
}
},
methods: {
checkTaskDone(item) {
if (this.checked == false) {
this.checked = true;
} else if (this.checked == true) {
this.checked = false;
}
}
}
}
.checked {
text-decoration: line-through;
color: #444;
}
With this code it adds the class to every single v-for list element regardless of which one is clicked, as expected. I'm wondering what's the best approach to deal with this. I've experimented with making a prop from the index and trying to target that to apply the styles but I couldn't make it work.
Thanks in advance!
Typically you want to have a "done" or "checked" flag on the individual to-do items, something like:
const todoList = [
{
name: 'Grab some food',
done: false
},
{
name: 'Start coding',
done: false
}
];
And in Vue.js, you could do the class toggling with v-bind:class rather than ternary operator:
export default {
data() {
return {
//checked: false,
activeList: {
listItems: [
{
name: 'Grab some food',
done: false
},
{
name: 'Start coding',
done: false
}
]
}
}
},
methods: {
checkTaskDone(item) {
//if (this.checked == false) {
// this.checked = true;
//}
//else if (this.checked == true) {
// this.checked = false;
//}
// Check/uncheck
item.done = !item.done;
}
}
}
<f7-list-item
v-for="(listItem, index) in activeList.listItems"
:key="index"
:class="{ 'checked': listItem.done }">
{{ index + 1 }}. {{ listItem }}
<span #click="checkTaskDone(listItem)">
<i class="f7-icons" :id="`check-task-btn${index}`">check_round</i>
</span>
</f7-list-item>
BTW, I'm appending an index on the individual i.f7-icons elements because ID should be unique, otherwise please use class instead.
First you need to create dynamic checked array according to activeList.listItems length ! Then you can check with index and you can update array data by this.$set(array,index,value) ...
new Vue({
el: "#app",
data: {
checked: [],
activeList : {listItems:[1,2,3,5]}
},
created: function() {
for(var i = 0; i < this.activeList.listItems.length; i++) {
this.checked.push(false);
}
},
methods: {
checkTaskDone(item) {
if (this.checked[item] == false) {
this.$set(this.checked,item, true);
} else if (this.checked[item] == true) {
this.$set(this.checked,item, false);
}
}
}
});
.checked {
text-decoration: line-through;
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(listItem, index) in activeList.listItems" :key="index" :class="{checked: checked[index]}">
{{ index+1 }}. {{ listItem }}
<span #click="checkTaskDone(index)">
<i class="f7-icons" id="check-task-btn">check_round</i>
</span>
</div>
</div>
Related
I have tried a lot of different things trying to stop event bubbling on the mousenter event but still have the problem, when I hover on a link the event triggers for all the links, like all of them were hovering at the same time. Is an unordered list of links and I'm using a computed value to dynamically change the inline styles when I hover the links:
My component is something like this:
<section class="categoryList">
<ul>
<li v-for="category in categories" :key="category.id">
<a
#mouseenter.stop="mouseOver()"
#mouseleave.stop="mouseOver()"
class="category"
:style="getStyle"
href="#"
#click.prevent="getCategory(category.name)"
>{{ category.name }}</a
>
</li>
</ul>
</section>
And this is the computed value:
const getStyle = computed(() => {
if (props.primaryColor != undefined && props.primaryColor != "") {
if (!hover.value) {
return `background-color:${props.primaryColor};color:${props.secondaryColor}`;
} else {
return `background-color:${props.secondaryColor};color:${props.primaryColor}`;
}
} else {
if (!hover.value) {
return `background-color:#614EFB;color:white`;
} else {
return `background-color:#523dfa;color:white`;
}
}
});
And then a standard function to control the hover state dynamically:
function mouseOver() {
hover.value = !hover.value;
}
If I understood you correctly try to set hover as id instead of boolean:
const { ref, computed } = Vue
const app = Vue.createApp({
setup() {
const props = ref({primaryColor: null, secondaryColor: null})
const categories = ref([{id: 1, name: 'aaa'}, {id: 2, name: 'bbb'}, {id: 3, name: 'ccc'}])
const hover = ref(null)
function mouseOver(id) { hover.value = id; }
function mouseExit() { hover.value = null }
getCategory = () => {}
const getStyle = (id) => {
if (props.primaryColor != undefined && props.primaryColor != "") {
if (id !== hover.value) {
return `background-color:${props.primaryColor};color:${props.secondaryColor}`;
} else {
return id === hover.value && `background-color:${props.secondaryColor};color:${props.primaryColor}`;
}
} else {
if (id !== hover.value) {
return `background-color:#614EFB;color:white`;
} else {
return `background-color:red;color:white`;
}
}
};
return { props, categories, mouseOver, getStyle, getCategory, hover, mouseExit }
}
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<section class="categoryList">
<ul>
<li v-for="category in categories" :key="category.id">
<a
#mouseenter="mouseOver(category.id)"
#mouseleave="mouseExit()"
class="category"
:style="getStyle(category.id)"
href="#"
#click.prevent="getCategory(category.name)"
>{{ category.name }}</a
>
</li>
</ul>
{{hover}}
</section>
</div>
I am trying my hands at Nuxt and having a problem that I can't seem to figure out.
The concept:
I am having two arrays, one called items and one called selectedItems. I loop through the array of items to showcase them, if I click them, I simply add them to the array of selectItems and if I click them again, then they are removed from the array. Now I also added a click variable which helps enable coloring in the text to showcase which are selected. I tried simplifying the example with array of [A,B,C] in the code snippet down below!
The problem:
Now in the page, you will already have existing items in the selectedItems like in my example code, whereas [B] already exists in the array. Now how would I on a pageload, set the objects in the array SelectItems, in this case B to have already been clicked? I tried doing a comparing of the items in array Item and array selectedItem via a computed and then a if statement on the component for a class. This worked sorta EXCEPT the element wasn't actually clicked, so you would have to click TWICE in order for the object to be removed in the array. So how do I make the objects from selectedItem be click=true?
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>
The code:
https://stackblitz.com/edit/nuxt-starter-cxakbv?file=pages/index.vue
You can try this way:
In index you pass selectedItems to Item component:
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
In Item component add new property selectedItem:
selectedItems: {
type: Array,
default: null,
},
And in mounted hook you check is item in selectItems array:
mounted() {
if (this.selectedItems.some(item => item.id === this.item.id)) this.clicked = true
}
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
selecteditems: {
type: Array,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
mounted() {
if (this.selecteditems.some(item => item.id === this.item.id)) this.clicked = true
}
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>
I have a loop where I loop over an Array.
for each item in this array I render a new component. Now when a user clicks on a certain component I only want to add a class to that component to highlight it and remove it from others that have it. Think of it as a menu active item.
<step-icon
v-for="(step, currentStep) in steps"
/>
data() {
return {
steps: [{foo: 'bar'}, {foo2: 'bar2'}]
}
}
my step-icon.vue:
<template>
<div :class="{'selected': selected}" #click="clickStep()">
hello
</div>
</template>
data() {
return {
selected: false
}
},
methods: {
clickStep() {
this.selected = true;
}
}
This works only 1 way, I can only add the selected class but never remove it.
I created a simple example illustrating your use case since you didn't provided enough detail to go with. Below you can find the items selected and unselected. Firstly, we added a key isSelected and set it to false as default. This will act as a status for all items.
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
Next, we looped over the array and displayed all the items.
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
Here you can se we have set our status property isSelected on v-bind directive which will add or remove the class based on the value of isSelected.
Next, once the item is clicked we will trigger select method.
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
The select method will firstly unselect all those except the one which is selected and then call toggleSelection which will set the selected Item to true or false.
Complete Code:
new Vue({
el: '#app',
data: {
steps: [
{key:"0", tec:"foo", isSelected:false},
{key:"1", tec:"bar", isSelected:false},
{key:"2", tec:"foo2", isSelected:false},
{key:"3", tec:"bar2", isSelected:false},
]
},
methods: {
select(key) {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i].key !== key) {
this.steps[i].isSelected = false
}
}
this.toggleSelection(key)
},
toggleSelection(key) {
const stepsItem = this.steps.find(item => item.key === key)
if (stepsItem) {
stepsItem.isSelected = !stepsItem.isSelected
}
}
}
})
.selected {
background: grey;
}
.notselected {
background:transparent;
}
<script src="https://unpkg.com/vue#2.6.10"></script>
<div id="app">
<ul>
<li
v-for="l in steps"
id="l.key"
#click="select(l.key, l.isSelected)"
v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
> {{ l.tec }} </li>
<ul>
</div>
You can keep all the step state in the parent component.
Now the parent component can listen for toggle_selected event from the nested one, and call toggle_selected(step) with the current step as a param.
Toggle_selected method should deselect all steps except the current one, and for the current one just toggle the selected prop.
If You would like to modify more props of the step in the nested component You could use .sync modifier (:step.sync="step") and then this.#emit('update:step', newStepState) in the nested component.
I've also made a snippet (my first). In this example I omitted clickStep and just put #click="$emit('toggle_selected') in the step-icon component.
new Vue({
el: '#app',
// for this example only defined component here
components: {
'step-icon': {
props: { step: Object }
}
},
data: {
steps: [
{ name: 'Alfa', selected: false},
{ name: 'Beta', selected: false},
{ name: 'Gamma', selected: false},
]
},
methods: {
toggle_selected(step) {
this.steps.filter(s => s != step).forEach(s => s.selected = false);
step.selected = true;
}
}
})
#app {
padding: 2rem;
font-family: sans-serif;
}
.step-icon {
border: 1px solid #ddd;
margin-bottom: -1px;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
.step-icon.selected {
background: #07c;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!-- Ive used inline-template for this example only -->
<div id="app">
<step-icon v-for="(step, currentStep) in steps" :key="currentStep"
:step.sync="step"
#toggle_selected="toggle_selected(step)" inline-template>
<div class="step-icon"
:class="{selected: step.selected}"
#click="$emit('toggle_selected')">
{{ step.name }}
</div>
</step-icon>
</div>
Context :
I am using VueJS in a project for the first time so I'm potentially not using it the right way.
I have a parent and a child component :
Parent : ResearchProducts.vue : Displays a list of products that can be filtred by their categories. The filters are checkboxes related to the category of the products.
Child : CategoryDisplay.vue : This is the component that handle the view and the methods of a category row.
My Question :
When I click on a category checkbox inside of my child component and if the checkbox is checked, the category is added to a list of filters in the parent. This works.
When I uncheck the checkbox, the category is removed from this list. This works too.
Now I've showed all my categories as buttons in the parent component to make the filters visible more easily for the user. And my problem is that I want this button to uncheck the related checkbox inside the child component when I click on it.
Here is my actual code :
ResearchProducts.vue :
<template>
<div>
<template v-for="category in categories">
<category-display
:category="category"
:key="'category_'+category.id"
:checked="checked"
#categorySelected="categorySelected"
></category-display>
</template>
<button
v-for="filter in listFilters"
:key="'filter_'+filter.slug"
class="btn btn-light btn-sm mr-2 mb-2"
#click="removeFilter(filter)"
>
{{ filter.name }}
</button>
</div>
</template>
<script>
export default {
data() {
return {
categories: { // HERE A COLLECTION CONTAINING ALL MY CATEGORIES },
selectedCategories: [],
listFilters: []
};
},
methods: {
categorySelected(category) {
var newCategory = {
type: "category",
slug: category.slug,
name: category.name
};
if (category.checked) {
if (!this.selectedCategories.includes(category.slug)) {
this.selectedCategories.push(category.slug);
this.listFilters.push(newCategory);
}
} else {
if (this.selectedCategories.includes(category.slug)) {
const index = this.selectedCategories.indexOf(category.slug);
if (index > -1) {
this.selectedCategories.splice(index, 1);
}
}
this.listFilters = this.listFilters.filter(function(item) {
for (var key in newCategory) {
if (item[key] === undefined || item[key] == newCategory[key])
return false;
}
return true;
});
}
},
removeFilter(filter) {
// THERE, I NEED TO UNCHECK THE RELATED CHECKBOX IN CHILD COMPONENT
this.listFilters = this.listFilters.filter(function(item) {
for (var key in filter) {
if (item[key] === undefined || item[key] == filter[key]) return false;
}
return true;
});
}
}
};
</script>
CategoryDisplay.vue :
<template>
<b-form-checkbox-group class="w-100">
<b-form-checkbox :value="category.slug" class="w-100" #input="selection" v-model="selected" ref="checked">
{{ category.name }}
<span class="badge badge-secondary float-right">{{ category.products_count }}</span>
</b-form-checkbox>
</b-form-checkbox-group>
</template>
<script>
export default {
props: {
category: {
type: Object,
required: true
}
},
data() {
return {
selected: false
}
},
methods: {
selection() {
var response = false;
if(this.selected.length !== 0){
response = true;
}
this.$emit('categorySelected', {slug: this.category.slug, name: this.category.name, checked: response});
}
}
};
</script>
Here is a simplified sample for your reference. You can use the sync modifier to achieve a two-way-binding between the parent and child. That together with a computed property in the child with a setter and getter
So passing all categories to child, and sync the selected categories:
<HelloWorld :cats.sync="selectedCategories" :categories="categories"/>
Child component takes the categories, iterates and shows checkboxes. Here we use the computed property, and when a checkbox is clicked, the setter emits the change to the parent:
<label v-for="c in categories" :key="c.id">
<input v-model="temp" type="checkbox" :value="c">
{{ c.name }}
</label>
script:
computed: {
temp: {
get: function() {
return this.cats;
},
set: function(newValue) {
this.$emit("update:cats", newValue);
}
}
}
And the parent just simply iterates the selectedCategories and as you wish, when a change happens in parent for selectedCategories, child will be automatically aware of when an item is deleted.
Here's a full sample for your reference: SANDBOX
I have a todo list in vue and I'm using pop() to clear out/delete list items. See the code in question below:
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: {
text: 'Hide'
},
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
removeItem: function() {
this.todos.pop();
},
toggleSeen: function() {
app.seen = !app.seen;
app.button.text = app.seen ? 'Hide' : 'Show';
}
}
});
.todo-list {
list-style-type: square;
}
.todo-list__delete {
display: none;
}
li:hover .todo-list__delete {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<ul class="todo-list">
<li v-for="todo in todos">
{{ todo.text }}
<a v-on:click="removeItem" class="todo-list__delete" href="#">Delete</a>
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>
The expected behavior is that when delete is clicked, it invokes the removeItem function and with the usage of this in that function it deletes the selected item. However what actually happens is it just deletes nodes starting from the bottom.
I thought the issue is that with this I'm actually referencing the delete link and not actually the <li> element I'm trying to remove. So I tried both:
removeItem: function() {
this.todos.parentElement.pop();
}
And:
removeItem: function() {
this.parentElement.todos.pop();
}
With no luck.
How does this work in Vue?
In that context, this refers to the Vue component (not the DOM element). this.todos is the todos array inside the component's data object, and pop removes the last item of an array. So that's why the last element is being removed.
If you want to remove a specific element you'll need to pass some information to the removeItem function about which element you want removed and then have removeItem() drop that specific element from the todos array instead of popping the last element. One simple way to do this would be to pass the array index to the removeItem function, and then splice that index out of the todos array:
<li v-for="(todo, index) in todos">
...
<a v-on:click="removeItem(index)">Delete</a>
</li>
removeItem: function(index) {
this.todos.splice(index, 1);
},
Your full snippet with that change applied is below:
// components
Vue.component('todoitem', {
template: "<li>Test Item</li>"
})
// app code
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'Sample Item 1' },
{ text: 'Sample Item 2' },
{ text: 'Sample Item 3' }
],
button: {
text: 'Hide'
},
seen: true
},
methods: {
addItem: function() {
let item = document.getElementById("list-input").value;
let error = document.getElementById("error");
if (item == "") {
error.style.display = "block";
} else {
app.todos.push({ text: item });
error.style.display = "none";
}
},
removeItem: function(index) {
this.todos.splice(index, 1);
},
toggleSeen: function() {
app.seen = !app.seen;
app.button.text = app.seen ? 'Hide' : 'Show';
}
}
});
.todo-list {
list-style-type: square;
}
.todo-list__delete {
display: none;
}
li:hover .todo-list__delete {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<ul class="todo-list">
<li v-for="(todo, index) in todos">
{{ todo.text }}
<a v-on:click="removeItem(index)" class="todo-list__delete" href="#">Delete</a>
</li>
</ul>
<input type="text" id="list-input">
<input type="submit" id="list-submit" v-on:click="addItem">
<span id="error" style="color: red; display: none;">Please Enter Text</span>
<ul>
<todoitem></todoitem>
</ul>
<h2 v-if="seen">SEEN</h2>
<button id="hide-seen" v-on:click="toggleSeen">{{ button.text }}</button>
</div>