v-if doesn't recognize value from array of object - vue.js

I'm working on a vueJS file and have a component :
<div class="panel-heading" v-on:click="displayValue(feature.id)">
{{ feature.nom }}
</div>
<div class="panel-body" v-if="getDisplay(feature.id)">
foo
</div>
my function displayValue(id) :
displayValue(id){
for(let i =0; i<this.product_site.displayFeatures.length;i++){
if (this.product_site.displayFeatures[i].idFeature === id){
this.product_site.displayFeatures[i].show = !this.product_site.displayFeatures[i].show
}
}
console.log(this.product_site.displayFeatures)
},
First I am not very happy with that. I trie to do a .find but it didn't work :
this.product_site.displayFeatures.find(function(a){
a.idFeature === id
}).show = true
But I had an error telling me can not read 'show' of undefined
and my function getDisplay(id)
getDisplay(id){
this.product_site.displayFeatures.forEach(function(a){
if(a.idFeature === id){
return a.show
}
})
}
same as before if I try with a find.
Anyway, I thought it would work with that, but when I do console.log(this.product_sites.displayFeatures) I have the array with the modified value but foo is not shown

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.

Value of input not changed

Using Vue3 and Vuex4
I got an input field:
<input :class="invalid.includes(item.attribute) ? 'invalidInput' : 'validInput'" type="text" :id="item.attribute" :name="item.attribute" :placeholder="item.default_value" min="0" step="any" :value="item.value" #input="validate(item.attribute, $event)" class="p-1">
I change the value of "invalid" like this. Just checking for the validity of a regex and adding/removing the attribute to the array.
VALIDATE_INPUT: (state, data) => {
var regex = /(?=.*\d)^\$?(([1-9]\d{0,2}(,\d{3})*)|0)?(\.\d{1,2})?$/;
switch (data.attribute) {
case 'invoice_amount_f':
if (!regex.test(data.value)) {
state.validations.push(data.attribute)
} else {
let index = state.validations.findIndex(el => el === data.attribute);
if (index > -1) {
state.validations.splice(index, 1);
}
}
break;
default:
break;
}
}
The action calling the mutation is called like:
const validate = (attribute, event) => {
store.dispatch('validate', {
attribute: attribute,
value: event.target.value
});
}
Computed:
var invalid = computed({
get() {
return store.getters.getValidationState;
}
});
When now typing something into the input field the text in the field ain't chaning. This seems to happen cause I use the value of invalid inside the template. Why is that?
EDIT: It seems to have something to do with the scope I am using it in.
<h3>{{ invalid }}</h3>
<div v-if="nestedListItems && Object.keys(nestedListItems).length !== 0">
<draggable v-model='nestedListItems' item-key="id" class=" w-12/12 bg-white m-auto border" animation="150">
When rendering it outside of draggable it's absolutely fine. Inside it crashes my store.
You need to provide an object to the :class, see here:
https://v3.vuejs.org/guide/class-and-style.html#binding-html-classes
I suggest you create a variable in the script containing the boolean e.g. isValid and then apply it like this:
:class="{invalidInput : !isValid, validInput: isValid }"

How to do summation in Vue

I'm quite new to coding (less than 3months old) and I'm currently trying to learn vue.
I'm trying out this simple exercise of doing a basic shopping cart and I want to get the total of all the product amounts. Here is my code:
HTML
<template>
<div class="product" #click="isMilkshown = true">{{ productList[0].name }} $ {{ productList[0].amount }}</div>
<div class="product" #click="isFishshown = true">{{ productList[1].name }} $ {{ productList[1].amount }}</div>
<div class="product" #click="isLettuceshown = true">{{ productList[2].name }} $ {{ productList[2].amount }}</div>
<div class="product" #click="isRiceshown = true">{{ productList[3].name }} $ {{ productList[3].amount }}</div>
<!-- Cart -->
<div class="main-cart">
<div>Cart</div>
<div class="main-cart-list" v-for="product in productList" :key="product">
<div v-if="showProduct(product.name)">{{ product.name }} $ {{ product.amount }}</div>
</div>
<div>Total: 0</div>
</div>
</template>
JS
export default {
data() {
return {
productList: [
{ name: "Milk", amount: 10 },
{ name: "Fish", amount: 20 },
{ name: "Lettuce", amount: 5 },
{ name: "Rice", amount: 2.5 }
],
isMilkshown: false,
isFishshown: false,
isLettuceshown: false,
isRiceshown: false
}
},
methods: {
showProduct(name) {
if (name === "Milk" && this.isMilkshown === false) {
return false
} else if (name === "Fish" && this.isFishshown === false) {
return false
} else if (name === "Lettuce" && this.isLettuceshown === false) {
return false
} else if (name === "Rice" && this.isRiceshown === false) {
return false
} else {
return true
}
}
}
}
I want to replace the "zero" in Total with the sum of all the product amounts when a product is clicked. Hope someone can help me, thanks!
You would use a computed function.
https://v2.vuejs.org/v2/guide/computed.html
In Vue, computed functions watch all the reactive variables referenced within them and re-run to update the returned value when any of those variables change.
Simply create a computed function that loops over each productList item and sums up the amount then returns it.
You can reference this answer to learn how to sum using reduce or for a standard example with a loop Better way to sum a property value in an array
Also, you can use a v-for loop on your
<div class="product" #click="isMilkshown = true">{{ productList[0].name }} $ {{ productList[0].amount }}</div>
component so that you don't have duplicated code:
<div v-for="item in productList" key="item.name" class="product">{{ item.name }} $ {{ item.amount }}</div>
This would create one of each of those elements for each item in your productList variable.
You would then need to re-write the click handler to be dynamic too.
Lastly, you can also convert your big if/else-if chained method into a computed function too so that you watch for changes in that.
To do that, you'd make the computed return an object like this:
{
Milk: true,
Fish: false
...
}
You can put the key as the name so that in your loop you can reference the computed property like this enabledItems[item.name] to get the true/false.

Vue: Dynamic images within a v-for

I am using a method in a v-for to determine what image to show. I am getting an empty source when I go to inspect the element though. Is this not a valid way to handle image rendering?
<div v-for="unit in units">
<img :src="getImageUnit(unit)/>
</div>
methods: {
getImageUnit(unitType) {
if (unitType == "single") {
return '/images/single.png';
} else {
return '/images/double.png';
}
}
}
As always, using methods to render parts of your template is highly inefficient
You should be using a computed property for this instead.
Assuming your image files are all in the public folder, try this
computed: {
unitsWithImages: ({ units }) => units.map(unit => ({
unit,
image: `${process.env.BASE_URL}images/${unit === "single" ? "single" : "double"}.png`
}))
}
<div v-for="unit in unitsWithImages">
<p>{{ unit.unit }}</p>
<img :src="unit.image" />
</div>

Nuxt Computed property "itemCost" was assigned to but it has no setter

I have a method like this:
methods: {
itemCost(item) {
let total = 0;
for(let choice of item.choices) {
if(choice.cost) total += choice.cost;
}
return total
}
}
And my vue file looks like this:
index.vue
<div class="container">
<div class="item" v-for="item in cart.items">
{{itemCost(item)}}
</div>
</div>
It's working great, however I get this warning I don't understand:
Computed property "itemCost" was assigned to but it has no setter.
It's a method, not a computed property so not sure why i'm getting the warning.
I looked at similar questions to this but all the answers mentioned that it was because it was a v-model so it needed a setter, in this case i'm trying to output the calculation so no user input is necessary.
Any ideas how to get rid of the warning?
It because of the weird construction of your component. You should do the calculation before, inside a computed, and then use it to print out:
computed: {
itemsWithCost: function() {
return this.cart.items.map( item => {
let total = 0;
for(let choice of item.choices) {
if(choice.cost) total += choice.cost;
}
item.total = total;
return item;
}
})
}
and in your template:
index.vue
<div class="container">
<div class="item"
v-for="(item,index) in itemsWithCost"
:key="index">
{{item.total}}
</div>
</div>