Vuejs setting reactive style to component in v-for with vuex data - vue.js

Not sure how to work this...
I'm displaying table rows, pulling my data with vuex. The style changes, but is not updated in the browser. Only when I reload or re-create the component, it shows the style change. Scratching my head on this and what would be the best way to set a reactive style that is ternary based off the data loaded in the v-for component ?
<tr #click="rowClick(item)" v-for="(item, index) in List" :style="[item.completed ? { 'background-color': 'red' } : { 'background-color': 'blue' }]" :key="index">
item.completed is a bool

I hope I was able to correctly reflect what you were trying to accomplish. I used a v-for on the template tag and after that line you could access the item.completed boolean value. I had to use a span element inside the tr to apply the styling.
Vue.createApp({
data() {
return {
List: [ // comes from your vuex
{
name: 'one',
completed: true,
},
{
name: 'two',
completed: false,
}
]
}
},
methods: {
rowClick(item) {
console.log(item)
}
}
}).mount('#demo')
<script src="https://unpkg.com/vue#next"></script>
<table id="demo">
<template v-for="(item, i) in List">
<tr>
<span #click="rowClick(item)" :style="item.completed ? { 'background-color': 'red' } : { 'background-color': 'blue' }">{{ i }}: {{ item.name }}</span>
</tr>
</template>
</table>
I think it could be solved a little bit cleaner. What do you think? Does it goes in the right direction?

Related

How to make single property in array reactive when using `ref` instead of `reactive`?

I have a component that displays rows of data which I want to toggle to show or hide details. This is how this should look:
This is done by making the mapping the data to a new array and adding a opened property. Full working code:
<script setup>
import { defineProps, reactive } from 'vue';
const props = defineProps({
data: {
type: Array,
required: true,
},
dataKey: {
type: String,
required: true,
},
});
const rows = reactive(props.data.map(value => {
return {
value,
opened: false,
};
}));
function toggleDetails(row) {
row.opened = !row.opened;
}
</script>
<template>
<div>
<template v-for="row in rows" :key="row.value[dataKey]">
<div>
<!-- Toggle Details -->
<a #click.prevent="() => toggleDetails(row)">
{{ row.value.key }}: {{ row.opened ? 'Hide' : 'Show' }} details
</a>
<!-- Details -->
<div v-if="row.opened" style="border: 1px solid #ccc">
<div>opened: <pre>{{ row.opened }}</pre></div>
<div>value: </div>
<pre>{{ row.value }}</pre>
</div>
</div>
</template>
</div>
</template>
However, I do not want to make the Array deeply reactive, so i tried working with ref to only make opened reactive:
const rows = props.data.map(value => {
return {
value,
opened: ref(false),
};
});
function toggleDetails(row) {
row.opened.value = !row.opened.value;
}
The property opened is now fully reactive, but the toggle doesn't work anymore:
How can I make this toggle work without making the entire value reactive?
The problem seems to come from Vue replacing the ref with its value.
When row.opened is a ref initialized as ref(false), a template expression like this:
{{ row.opened ? 'Hide' : 'Show' }}
seems to be interpreted as (literally)
{{ false ? 'Hide' : 'Show' }}
and not as expected as (figuratively):
{{ row.opened.value ? 'Hide' : 'Show' }}
But if I write it as above (with the .value), it works.
Same with the if, it works if I do:
<div v-if="row.opened.value">
It is interesting that the behavior occurs in v-if and ternaries, but not on direct access, i.e. {{ rows[0].opened }} is reactive but {{ rows[0].opened ? "true" : "false" }} is not. This seems to be an issue with Vue's expression parser. There is a similar problem here.

Cannot get computed property (array)

Trying to get a 'displayImages' array as a computed property. Using a default 'selected' property = 0.
this.selected changes accordingly on mouseover and click events.
When trying to get the computed 'displayImages' it says:
"this.variations[this.selected] is undefined."
I'm using an api to get my product data and images.
<template>
<div id="product-page">
<v-card width="100%" class="product-card">
<div class="image-carousel">
<v-carousel height="100%" continuos hide-delimiters>
<v-carousel-item
v-for="(image, i) in displayImages"
:key="i"
:src="image"
>
</v-carousel-item>
</v-carousel>
</div>
<div class="details">
<h2>{{ this.title }}<br />Price: ${{ this.price }}</h2>
<p>{{ this.details }}</p>
<ul style="list-style: none; padding: 0">
<li
style="border: 1px solid red; width: auto"
v-for="(color, index) in variations"
:key="index"
#mouseover="updateProduct(index)"
#click="updateProduct(index)"
>
{{ color.color }}
</li>
</ul>
<div class="buttons">
<v-btn outlined rounded
>ADD TO CART<v-icon right>mdi-cart-plus</v-icon></v-btn
>
<router-link to="/shop">
<v-btn text outlined rounded> BACK TO SHOP</v-btn>
</router-link>
</div>
</div>
</v-card>
</div>
</template>
<script>
export default {
name: "Product",
props: ["APIurl"],
data: () => ({
title: "",
details: "",
price: "",
variations: [],
selected: 0,
}),
created() {
fetch(this.APIurl + "/products/" + this.$route.params.id)
.then((response) => response.json())
.then((data) => {
//console.log(data);
this.title = data.title;
this.details = data.details.toLowerCase();
this.price = data.price;
data.variations.forEach((element) => {
let imagesArray = element.photos.map(
(image) => this.APIurl + image.url
);
this.variations.push({
color: element.title,
images: imagesArray,
qty: element.qty,
productId: element.productId,
});
});
});
},
computed: {
displayImages() {
return this.variations[this.selected].images;
},
},
methods: {
updateProduct: function (index) {
this.selected = index;
console.log(index);
}
},
};
</script>
To properly expand on my comment, the reason why you are running into an error is because when the computed is being accessed in the template, this.variations is an empty array. It is only being populated asynchronously, so chances are, it is empty when VueJS attempts to use it when rendering the virtual DOM.
For that reason, accessing an item within it by index (given as this.selected) will return undefined. Therefore, attempting to access a property called images in the undefined object will return an error.
To fix this problem, all you need is to introduce a guard clause in your computed as such:
computed: {
displayImages() {
const variation = this.variations[this.selected];
// GUARD: If variation is falsy, return empty array
if (!variation) {
return [];
}
return variation.images;
},
}
Bonus tip: if you one day would consider using TypeScript, you can even simplify it as such... but that's a discussion for another day ;) for now, optional chaining and the nullish coalescing operator is only supported by bleeding edge versions of evergreen browsers.
computed: {
displayImages() {
return this.variations[this.selected]?.images ?? [];
},
}
For avoid this kind of error, you must to use the safe navigation property.
Remember, it's useful just when the app is loading.
Try something like that:
<script>
export default {
name: 'Product',
computed: {
displayImages() {
if (this.variations[this.selected]) {
return this.variations[this.selected].images;
}
return [];
},
},
};
</script>

VueJS : reverse data array has no effect

In this fiddle : https://jsfiddle.net/djsuperfive/svctngeb/ I want to reverse the structure array in data when I click the button.
Why are the rendered list and the dump of structure not reactive whereas the console.log reflect that the reverse was effective ?
The code :
HTML
<div id="app">
<draggable v-model="structure">
<div v-for="(item, index) in structure" :style="'background-color:'+item.color">
{{ item.title }}
</div>
</draggable>
<button type="button" #click="reverse()">Reverse structure</button>
<hr>
<strong>dump structure:</strong>
<pre>
{{ structure }}
</pre>
</div>
JS:
new Vue({
el: '#app',
data() {
return {
structure: [{
title: 'Item A',
color: '#ff0000'
},
{
title: 'Item B',
color: '#00ff00'
},
{
title: 'Item C',
color: '#0000ff'
},
],
}
},
methods: {
reverse() {
console.log(this.structure[0].title);
_.reverse(this.structure);
console.log(this.structure[0].title);
}
}
});
thanks
Max!
Try to replace line:
_.reverse(this.structure);
with
this.structure.reverse();
I guess, Underscore.js does not have reverse method, because JavaScript has native one.
Everything should work fine with native JS Array reverse function.
Good luck!
The problem here seems to be that Vue does not detect that your array has changed: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
The solution is to either replace this.structure with a new array or to have a reverse function that uses Vue.set().
this.structure = _.clone(_.reverse(this.structure));

Render block in v-for over object only once vue.js?

I have an object with this structure
object: {
"prop1": [],
"prop2": [],
"prop3": [],
}
In my template I want to loop over it and display data in prop's but if there is no data in any of them I want to show something like this
<div>No data</div>
but only once and not for each prop
So far I have this
<div v-for="(props, index) in object" :key="index">
<div v-if="!props.length">
No data
</div>
</div>
But it shows message 3 times, for each prop.
I'm not sure how to solve it. Any help will be much appreciated.
To solve this in a nice way, you should use a computed property.
A computed property is basically a piece of code that give meaningless caclulations meaningful names.
export default {
data() {
return {
object: {
"prop1": [],
"prop2": [],
"prop3": [],
},
};
},
computed: {
areAllEmpty() {
return Object.values(this.object).map(e => e.length).reduce((a, b) => a + b, 0) === 0;
},
}
};
This can then be used in your template as the following:
<template>
<template v-if="areAllEmpty">
<div>No data</div>
</template>
<template v-else>
<div v-for="(props, index) in object" :key="index">
I'm index {{ index }} with length {{ props.length }}
</div>
</template>
</template>

Move elements passed into a component using a slot

I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>