I have a vue project where I'm loading an array on page load and looking at the line item of each, checking the status and trying to show a button for each status with a 3-way toggle.
I think I have the main idea right but the structure is off with the mounting and it being an array. I'm a bit stuck on how to fully get this working but quite simply I want only one button to show for each subItem row based on status.
If subItem status = 'A' I want a button that says Accept, if it's 'B' then pause, and if 'C' then resume. I need the toggle to work and I can then work on calling an axios call based on the status, but I think I just need this working first to get the idea.
subItems is an array like this:
array(2)
0:
id: 123,
status: 'A'
1:
id: 234,
status: 'B'
This is my template/vue code:
<div class="row" v-for="(subItem, key) in subItems" v-cloak>
<button :class="['btn', 'btn-block', subItem.status == 'H' ? 'accept' : 'resume' : 'pause']" :style="{ border:none, borderRadius: .15 }" v-on:click="pause(subItem)" type="button" role="button" id="" aria-expanded="false">
{{ subItem.status == 'A' ? 'Accept' : 'Resume' : 'Pause' }}
</button>
</div>
data() {
return {
subItems: [],
}
}
You can use a computed property to extend the property on the data object, or you could do this is the mounted method. A computed property will be better as it will change when the data object does.
new Vue({
el: '#app',
computed: {
formattedSubItems() {
return this.subItems.map(si => {
if (si.status === 'A') {
return { ...si,
text: 'Accept',
class: 'accept'
}
} else if (si.status === 'B') {
return { ...si,
text: 'Pause',
class: 'pause'
}
} else if (si.status === 'C') {
return { ...si,
text: 'Resume',
class: 'resume'
}
}
})
}
},
data() {
return {
subItems: [{
id: 123,
status: 'A'
},
{
id: 234,
status: 'B'
}
],
}
}
})
.accept {
color: green
}
.pause {
color: yellow
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="row" v-for="(subItem, key) in formattedSubItems" v-cloak>
<button class="btn btn-block" :class="subItem.class" :style="{ border:none, borderRadius: .15 }" v-on:click="pause(subItem)" type="button" role="button" id="" aria-expanded="false">
{{ subItem.text}}
</button>
</div>
</div>
You could also create a button object that contain your button name with based on your key. Like below example
buttons: {
A: 'Accept',
B: 'Pause',
C: 'Resume'
}
And this buttons object you can use when you looping your subItems.
Please check below working code snippet.
new Vue({
el: '#app',
methods: {
getClass(subItem) {
return this.buttons[subItem.status].toLocaleLowerCase();
},
pause(subItem) {
console.log(this.buttons[subItem.status])
}
},
data() {
return {
subItems: [{
id: 123,
status: 'A'
}, {
id: 234,
status: 'B'
}, {
id: 235,
status: 'C'
}],
buttons: {
A: 'Accept',
B: 'Pause',
C: 'Resume'
}
}
}
})
.accept {
color: green
}
.pause {
color: violet
}
.resume {
color: red
}
.btn-block {
cursor: pointer;
border: 1px solid;
padding: 5px 10px;
margin: 10px;
font-size: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="row" v-for="(subItem, key) in subItems">
<button class="btn btn-block" :class="getClass(subItem)" #click="pause(subItem)" role="button" aria-expanded="false">
{{ buttons[subItem.status]}}
</button>
</div>
</div>
Related
I am trying to create a v-for that shows a list of exercises containing several sets. I have created a loop with a row for each set underneath each exercise.
my data looks like this.
const exercises = [
{ id: 1, name: exercise1, sets: 3 },
{ id:2, name: exercise2, sets: 2 }
{ id:3, name: exercise3, sets: 4 }
]
And my component looks something like this:
<template v-for="exercise in exercises" :key="exercise.id">
<span> {{ exercise.name }} </span>
<template v-for="set in exercise.sets" :key="set">
<span #click="completeSet()"> {{ set }} </span>
</template>
</template>
Now I want to be able to mark each set as completed by setting the value on each set to either true or false through a click event. But I am not sure about how to do this since each set doesn't have a property to set a value because it's looping through a number.
What would be the right approach to this problem?
First and foremost, you can't loop through a number. To be able to loop the sets, you'd have to
<template v-for="let set = 0; set < exercise.sets; set++" :key="set">
<span #click="completeSet()"> {{ set }} </span>
</template>
However, setting a property on a number is equally impossible. You have to prepare your data to be able to make that adjustment:
const exercises = [
{ id: 1, name: 'exercise1', sets: 3 },
{ id: 2, name: 'exercise2', sets: 2 } ,
{ id: 3, name: 'exercise3', sets: 4 }
].map(exercise => ({
id: exercise.id,
name: exercise.name,
sets: Array.from(
{ length: exercise.sets },
() => ({ completed: false })
),
}))
You can create array with finished sets and compare it (try the snippet pls):
new Vue({
el: "#demo",
data() {
return {
exercises: [{ id: 1, name: 'exercise1', sets: 3 }, { id: 2, name: 'exercise2', sets: 2 }, { id: 3, name: 'exercise3', sets: 4 }],
finishedSets: []
}
},
computed: {
checkAll() {
return this.exercises.reduce((acc, curr) => acc + curr.sets, 0) === this.finishedSets.length
}
},
methods: {
compareObjects(o1, o2) {
return Object.entries(o1).sort().toString() !== Object.entries(o2).sort().toString()
},
findObject(id, set) {
return this.finishedSets.find(f => f.id === id && f.set === set)
},
completeSet(id, set) {
this.findObject(id, set) ?
this.finishedSets = this.finishedSets.filter(f => {return this.compareObjects(f, this.findObject(id, set))}) :
this.finishedSets.push({id, set})
},
isFinished(id, set) {
return this.findObject(id, set) ? true : false
},
}
})
.set {
width: 70px;
cursor: pointer;
}
.finished {
background-color: seagreen;
}
.finished__not {
background-color: tomato;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div v-for="exercise in exercises" :key="exercise.id">
<span> {{ exercise.name }} </span>
<div v-for="set in exercise.sets" :key="set">
<div #click="completeSet(exercise.id, set)" class="set" :class="isFinished(exercise.id, set) ? 'finished' : 'finished__not'"> {{ set }} <span>
<span v-if="isFinished(exercise.id, set)">finished</div>
</div>
</div>
<button v-if="checkAll">submit</button>
<p>{{finishedSets}}</p>
</div>
i need to add an input field used to edit the title in the currently selected element component (selection was done by clicking). The problem is that there should be one input and work for each selected element. I couldn't find a similar task and solving on the Internet. Maybe someone will tell you how to do it?
ItemsList.vue component:
<template>
<input type="text" placeholder="Edit selected items"/>
<div class="items-col">
<ul class="items-list">
<Item v-for="item in items" :key="item" :title="item.title"/>
</ul>
</div>
</template>
<script>
import Item from '#/components/Item.vue'
export default {
data() {
return {
items: [
{ title: 'item 1' },
{ title: 'item 2' },
{ title: 'item 3' },
{ title: 'item 4' },
{ title: 'item 5' },
{ title: 'item 6' }
]
}
},
components: {
Item
}
}
</script>
Item.vue component:
<template>
<li class="item" #click="isActive = !isActive" :class="{ active: isActive }">{{ title }}</li>
</template>
<script>
export default {
name: 'ItemsList',
data() {
return {
isActive: false
}
},
props: {
title: String
}
}
</script>
<style>
.item.active {
color: red;
}
</style>
You might want to reconsider which component should be responsible of knowing which item is active at any point of time: hint: it should be the parent/consuming component. That is because you:
Have only a single input field, which means only one item can be edited at any point of time
You want to let the parent/consuming component to be the single source of truth of which item is actually active
Therefore, the first thing you should do is to ensure that isActive is a prop on the Item component, while the parent ItemList component keeps track of which is active at any point.
Then, it is simply a matter of:
Implementing a toggling logic for the isActive flag. The flag is updated when a native click event is fired from the Item component. For the toggling logic, we can simply toggle between a zero-based index of the click item, or -1, which we used to indicate that nothing is active.
Using v-bind:value and a computed property to reflect the value of the currently active item. We can simply retrieve it using this.items[this.activeIndex] on the parent component
Listening to the onInput event and then updating the correct item
See proof-of-concept below:
Vue.component('item-list', {
template: '#item-list-template',
data() {
return {
items: [{
title: 'item 1'
},
{
title: 'item 2'
},
{
title: 'item 3'
},
{
title: 'item 4'
},
{
title: 'item 5'
},
{
title: 'item 6'
}
],
activeIndex: -1,
}
},
methods: {
onItemClick(index) {
this.activeIndex = this.activeIndex === index ? -1 : index;
},
setActiveItemValue(event) {
const foundItem = this.items[this.activeIndex];
if (!foundItem) return;
return this.items[this.activeIndex].title = event.currentTarget.value;
}
},
computed: {
activeItemValue() {
return this.items[this.activeIndex]?.title ?? '';
}
}
});
Vue.component('item', {
template: '#item-template',
props: {
isActive: Boolean,
title: String
}
});
new Vue({
el: '#app'
});
li.active {
background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<item-list></item-list>
</div>
<script type="text/x-template" id="item-list-template">
<div>
<input type="text" placeholder="Edit selected items" :value="activeItemValue" #input="setActiveItemValue" />
<div class="items-col">
<ul class="items-list">
<item v-for="(item, i) in items" :key="i" :title="item.title" :isActive="activeIndex === i" #click.native="onItemClick(i)" />
</ul>
</div>
</div>
</script>
<script type="text/x-template" id="item-template">
<li class="item" :class="{ active: isActive }">{{ title }}</li>
</script>
If you want a solution with your current components (not the cleanest) , you can actually emit an event to the parent component when you activate an element that event should containe the index of the object in the items array
Then you can use the index to get and set the title variable , here is an example :
Item.vue
<template>
<li class="item" #click="activateItem" :class="{ active: isActive }">{{ title }}</li>
</template>
<script>
export default {
name: 'ItemsList',
data() {
return {
isActive: false
}
},
methods:{
activateItem() {
this.isActive = !this.isActive
this.$emit('activatedItem', this.isActive ? this.index : null)
}
},
props: {
title: String,
index: Number
}
}
</script>
<style>
.item.active {
color: red;
}
</style>
ItemList.vue
<template>
<div>
<input type="text" placeholder="Edit selected items" #input="inputChange" :value="inputValue"/>
<div class="items-col">
<ul class="items-list">
<Item v-for="(item, index) in items" :key="index" :title="item.title" :index="index" #activatedItem="itemSelected"/>
</ul>
</div>
</div>
</template>
<script>
import Item from '#/components/Item.vue'
export default {
data() {
return {
items: [
{ title: 'item 1' },
{ title: 'item 2' },
{ title: 'item 3' },
{ title: 'item 4' },
{ title: 'item 5' },
{ title: 'item 6' }
],
selectedIndex: null,
inputValue: ''
}
},
methods:{
itemSelected(index){
this.selectedIndex = index;
if(this.selectedIndex != null) {
this.inputValue = this.items[this.selectedIndex].title;
}
},
inputChange(event){
this.inputValue = event.target.value;
if(this.selectedIndex != null){
this.items[this.selectedIndex].title = this.inputValue
}
}
},
components: {
Item
}
}
</script>
You should also be aware that with the component Item you have given you can select more than one item !
I am really confused.. I have a list of items with a heart-like "like" button. After the icon's clicked on I would like to change the color. But after this all my hearts change their colors, not only the one, which I pressed. Should I pass some argument in markAsFavorite method? Like index, or book?
<template>
<v-flex v-for="(book, index) in allBooks">
<div>Title: {{ book.title }}</div>
<i #click="markAsFavorite()" :class="{isActive: isMark}" class="fas fa-heart"></i>
</template
<script>
name: 'Books',
data () {
return {
allBooks: [
{ title: "one" },
{ title: "two" },
{ title: "three" },
{ title: "four" },
],
isMark: false,
}
},
methods: {
markAsFavorite() {
this.isMark = !this.isMark
},
}
</script>
Every object should have it property isMark as below:
UPDATE: you can merge properties to your original data after be fetched:
const myData = [
{ title: "one"},
{ title: "two" },
{ title: "three" },
{ title: "four" },
]
new Vue({
el: '#app',
data() {
return {
isLoading: true,
allBooks: []
}
},
created() {
this.fetchAPI()
},
methods: {
fetchAPI() {
setTimeout(() => {
this.allBooks = myData.map(item => ({...item, isMark: false }))
this.isLoading = false
}, 2000)
},
markAsFavorite(book) {
book.isMark = !book.isMark
}
}
})
.my-icon {
cursor: pointer;
margin-left: 4px;
}
.isActive {
color: red;
}
.flex {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/js/all.min.js"></script>
<div id="app">
<span v-if="isLoading">
loading
<i class="fas fa-spinner fa-spin"></i>
</span>
<template v-else>
<div class="flex" v-for="(book, index) in allBooks" :key="index">
<div>Title: {{ book.title }}</div>
<span
:class="[
{ isActive: book.isMark },
'my-icon'
]"
#click="markAsFavorite(book)"
>
<i class="fas fa-heart"></i>
</span>
</div>
</template>
</div>
I would separate out each book into its own component. That way it can track its own isMark
Book.vue
<template>
<div>
<div>Title: {{ book.title }}</div>
<i #click="markAsFavorite" :class="{isActive: isMark}" class="fas fa-heart"></i>
</div>
</template>
<script>
export default {
name: 'Book',
props: { book: Object },
data: () => ({ isMark: false }),
methods: {
markAsFavorite () {
this.isMark = !this.isMark
this.$emit(this.isMark ? 'marked' : 'unmarked', this.book)
}
}
}
</script>
and then in your Books component
<template>
<v-flex v-for="(book, index) in allBooks" :key="index">
<Book :book="book" #marked="handleMarked" #unmarked="handleUnmarked" />
</v-flex>
</template>
<script>
import Book from './Book.vue'
export default {
name: 'Books',
components: { Book },
// and so on
}
</script>
How do I rotate just that arrow icon based on the clicked item?
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1"
},
{
id: 2,
name: "Test2"
},
{
id: 3,
name: "Test3"
},
{
id: 4,
name: "Test4"
},
]
}
},
methods: {
arrowToggle() {
this.isToggled = !this.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});
i {
border: solid black;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
}
.down {
transform: rotate(45deg);
}
.up {
transform: rotate(-155deg);
}
.accordion {
display: flex;
background: lightblue;
align-items: center;
width: 100%;
width: 1000px;
justify-content: space-between;
height: 30px;
padding: 0 20px;
}
.arrow {
transform: rotate(-135deg);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle()">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled }" class="arrow"> </i>
</div>
</div>
</div>
Based on the clicked element do I want my arrow to rotate?
If i have 10 items and click on 2 items i want the icon to rotate there.
Failing to bind id to the clicked item and to bind that class to rotate the item
One thing is very important, I cannot set the isOpen parameter in my json ITEMS which is false which everyone recommends to me. I get it from a database and I don't have a condition for it.
You will have to toggle at individual item level. Note that I have used isToggled per item. Here is full code at: https://jsfiddle.net/kdj62myg/
Even if you get your items from DB, you can iterate through array and add a key named isToggled to each item.
HTML
<div id="app" style="display: flex; justify-content: center; align-items: center;">
<div v-for="(item, index) in items" :key="index">
<div class="accordion" #click="arrowToggle(item)">
<p> {{ item.name }}</p>
<i :class="{ 'down': item.isToggled, 'up': !item.isToggled }"> </i>
</div>
</div>
</div>
Vue
new Vue({
el: "#app",
data() {
return {
isToggled: false,
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
}
},
methods: {
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
getItems() {
return this.items;
}
},
mounted() {
this.getItems();
}
});
You have to map your items and attach a custom data on it to solve your problem.
Items data should be like this
items: [{
id: 1,
name: "Test1",
isToggled: false
},
{
id: 2,
name: "Test2",
isToggled: false
},
{
id: 3,
name: "Test3",
isToggled: false
},
{
id: 4,
name: "Test4",
isToggled: false
},
]
and your toogle function should look like this.
arrowToggle(item) {
return item.isToggled = !item.isToggled;
},
Now, after you fetched the items from the server. You have to map it to attach a isToggled data on every item you have. like this.
getItems() {
axios.get('api/for/items')
.then(({data}) => {
this.items = data.map(item => ({
return {
name:item.name,
id:item.id,
isToggled:false
}
}))
});
}
The above arrowToggle function breaks vue reactivity (google vue reactivity for docs). According to the docs, changing an object property directly will break reactivity. To keep reactivity, the function should change to:
arrowToggle(item) {
this.$set(this.item, 'isToggled', item.isToggled = !item.isToggled)
return item.isToggled;
},
Here is an example fiddle:
https://jsfiddle.net/40fxcuqd/
Initially, it displays "Carl"
If I select Carol, Clara etc, then an event will fire and data will print to the console.
But if I click the dropdown and choose "Carl", no event will fire, and nothing will print to the console.
The event I'm using is #input:
<select v-model="selectedPerson" #input="myEvent()">
How can I get an event to fire every time something is selected, even if it's the same value?
Edit:
To clarify, when "Carl" is initially selected:
and then the dropdown is opened:
and then Carl is selected again, I would like an event to be triggered and a print to the console. My issue at the moment is no event is triggered, and nothing prints to the console.
That is because the selected option by default is 1, then nothing change when you click on Carl, you must use #change event and if you want to get Carl value when you do click should use placeholder on select option.
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #change="myEvent()">
<option :value="null" disabled hidden>Select option</option>
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [
{key: 1, name: "Carl"},
{key: 2, name: "Carol"},
{key: 3, name: "Clara"},
{key: 4, name: "John"},
{key: 5, name: "Jacob"},
{key: 6, name: "Mark"},
{key: 7, name: "Steve"}
],
selectedPerson: null
},
methods: {
myEvent: function() {
console.log(this.selectedPerson);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
Really hacky but does the job, I've used #click and event.detail or event.which:
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #input="myEvent($event)" #click="myEvent($event)">
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [{
key: 1,
name: "Carl"
},
{
key: 2,
name: "Carol"
},
{
key: 3,
name: "Clara"
},
{
key: 4,
name: "John"
},
{
key: 5,
name: "Jacob"
},
{
key: 6,
name: "Mark"
},
{
key: 7,
name: "Steve"
}
],
selectedPerson: 1
},
methods: {
myEvent: function(e) {
if (e.detail == 0)//if (e.which == 0)
console.log(e.type, this.selectedPerson);
}
}
});
body {
margin: 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.as-console-wrapper {
height: 39px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app"></div>
A less hacky way using data:
new Vue({
el: '#app',
template: `
<div>
<select v-model="selectedPerson" #input="myEvent($event)" #click="myEvent($event)">
<option v-for="person in people" :value="person.key" :selected="person.key == selectedPerson">{{person.name}}</option>
</select>
</div>
`,
data: {
people: [{
key: 1,
name: "Carl"
},
{
key: 2,
name: "Carol"
},
{
key: 3,
name: "Clara"
},
{
key: 4,
name: "John"
},
{
key: 5,
name: "Jacob"
},
{
key: 6,
name: "Mark"
},
{
key: 7,
name: "Steve"
}
],
selectedPerson: 1,
prev: 0,
isChanged: false
},
methods: {
myEvent: function(e) {
if (e.type == "input" || (e.type == "click" && !this.isChanged && (this.prev == this.selectedPerson || this.prev == 0))) {
this.isChanged = true;
this.prev = 0;
} else if (e.type == "click" && this.isChanged) {
console.log(e.type, this.selectedPerson);
this.prev = this.selectedPerson;
this.isChanged = false;
}
}
}
});
body {
margin: 20px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.as-console-wrapper {
height: 39px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app"></div>
when you change the dropdown you will get the index of people array and you can do something like this to get value
myEvent: function() {
console.log(this.people[this.selectedPerson].name);
}
}
one workaround is to set selected to a not used value when focus, then change event will fire no matter which option is selected.
<select
v-model="selectedPerson"
ref="s"
#focus="selectedPerson = 0"
#change="myEvent()"
>
see fiddle: https://jsfiddle.net/tne1wp3q/
it's not perfect though, the change event will be fired multiple times with each click, and if no options were selected, it could left blank. Need more code to filter these behaviour.