How to do summation in Vue - vue.js

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.

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.

Vue 2 list band members by instrument name but only display the instrument name once

I am able to list the band members by instrument name, but want to display the instrument name only once, instead of showing the instrument name for each person in the bassoon group, for instance. My thought was to extract a set of distinct instrument names, and do a v-if comparing the member's instrument name to the iterated set value, and only display the member if the member's data property instrumentName matches the current value of the instrument names set. With the code below, I'm getting an error "instNames is not defined". I'm still learning Vue and Nuxt, and am trying to get away from WebMatrix, the original platform for my website. The other thing I need is to attach an instrumentFilename (picture) to each distinct instrumentName. Any help is greatly appreciated!
<template>
<div class="block bg-blue-200 w-96 p-6 m-auto">
<table>
<tr v-for="inst in instNames">
<td>{{ inst.instrumentName }} <img :src="'/images/' + inst.instrumentFilename" >
<td v-for="member in members" :key="member.id">
<div v-if="member.instrumentName == inst.instrumentName"> </div>
<div>{{ member.firstname }} {{ member.lastname }} </div>
<div v-if="$auth.loggedIn" class="block">
<p v-if="!member.email == ' '" class="ml-10"> Email: {{ member.email }} </p>
<p v-if="!member.telephone == ' '" class="ml-10"> Home: {{ member.telephone }} </p>
<p v-if="!member.telephone2 == ' '" class="ml-10">Cell: {{ member.telephone2 }}</p>
<p v-if="!member.street == ' '" class="ml-10">Address: {{ member.street}}</p>
<p v-if="!member.city == ' '" class="ml-28">{{ member.city}}, {{ member.zip}}</p>
</div>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
async asyncData(context){
const { data } = await context.$axios.get('/api/members')
return {
members : data
}
},
computed: {
instNames: function() {
instNames = [...new Set(this.members.map(x => x.instrumentName))]
return instNames
}
}
}
</script>
The above comments were invaluable, so a million thanks. I still had trouble with the uniqueness of my return array though; apparently, a Set counts multiple references to an object as unique. So I wound up using this code:
computed: {
instNames: function() {
const instName = this.members.map
(({ instrumentName, instrumentFilename }) => ({ instrumentName, instrumentFilename}))
const instNameArray = []
const instNameSet = new Set()
for (const object of instName) {
const objectJSON = JSON.stringify(object)
if(!instNameSet.has(objectJSON)) {
instNameArray.push(object)
}
instNameSet.add(objectJSON)
}
console.log(instNameArray)
return instNameArray
}
}
Your computed property tries to assign instNames, but that's not declared anywhere. However, you don't really need the local variable because it's not used. You can just return the calculated set:
export default {
computed: {
instNames: function() {
//instNames = [...new Set(this.members.map(x => x.instrumentName))]
//^^^^^^^^^ ❌ not declared
// Option 1: Declare it with `let` or `const`
const instNames = [...new Set(this.members.map(x => x.instrumentName))]
return instNames
// Option 2: Just return the calculation
return [...new Set(this.members.map(x => x.instrumentName))]
}
}
}
But your template tries to render instrumentName and instrumentFilename off of the computed instNames (an array of strings, not objects). Assuming those values are from this.members, you could update the computed value to return those fields:
export default {
computed: {
instNames: function() {
const instName = this.members.map(x => ({
instrumentName: x.instrumentName,
instrumentFilename: x.instrumentFilename,
}))
// remove duplicates here ...
return instName
}
}
}
Lastly, your template v-if conditions aren't properly checking for non-empty strings. For !member.email == ' ', the ! operator has precedence, and it checks member.email for a falsy value, which is the empty string for String. Then it compares it to a single space, which would always be false because a space is not falsy. You can replace that entire comparison with a simple check for a truthy value:
<p v-if="member.email">...</p>
<p v-if="member.telephone">...</p>
<p v-if="member.telephone2">...</p>
<p v-if="member.street">...</p>
<p v-if="member.city">...</p>

Is there a way to add line breaks in b-form-checkbox-group?

I'm currently trying to add line breaks to this Vue Bootstrap form checkbox group. The example that Vue Bootstrap docs (https://bootstrap-vue.js.org/docs/components/form-checkbox/) show how I can create a select all checkboxes group, but they are tightly grouped (without line breaks). Is there a way that I could add a line break within the flavours array to separate each object?
I have tried using v-html on a div tag and load in the flavours array, but it did not have the right outcome. I also tried :style="{marginBottom:'25px'}" and it also didn't work, the entire group of array move up and down the page together. I ran out of ideal after that.
HTML
<template>
<div>
<b-form-group>
<template slot="label">
<b>Choose your flavours:</b><br>
<b-form-checkbox
v-model="allSelected"
:indeterminate="indeterminate"
aria-describedby="flavours"
aria-controls="flavours"
#change="toggleAll"
>
{{ allSelected ? 'Un-select All' : 'Select All' }}
</b-form-checkbox>
</template>
<b-form-checkbox-group
id="flavors"
v-model="selected"
:options="flavours"
name="flavors"
class="ml-4"
aria-label="Individual flavours"
stacked
></b-form-checkbox-group>
</b-form-group>
<div>
Selected: <strong>{{ selected }}</strong><br>
All Selected: <strong>{{ allSelected }}</strong><br>
Indeterminate: <strong>{{ indeterminate }}</strong>
</div>
</div>
</template>
JavasScript
<script>
export default {
data() {
return {
flavours: ['Orange', 'Grape', 'Apple', 'Lime', 'Very Berry'],
selected: [],
allSelected: false,
indeterminate: false
}
},
methods: {
toggleAll(checked) {
this.selected = checked ? this.flavours.slice() : []
}
},
watch: {
selected(newVal, oldVal) {
// Handle changes in individual flavour checkboxes
if (newVal.length === 0) {
this.indeterminate = false
this.allSelected = false
} else if (newVal.length === this.flavours.length) {
this.indeterminate = false
this.allSelected = true
} else {
this.indeterminate = true
this.allSelected = false
}
}
}
}
</script>
Don't use the options array. Render each option as it's own <b-form-checkbox> inside of the <b-form-checkbox> group, and on each <b-form-checkbox> add the class mb-1, mb-2, mb-3, mb-4, or mb-5 (these are margin bottom spacing helper classes).
<b-form-checkbox-group
id="flavours"
v-model="selected"
name="flavours"
class="ml-4"
aria-label="Individual flavours"
stacked
>
<b-form-checkbox
v-for="flavour in flavours"
:value="flavour"
class="mb-5"
>
{{ flavour }}
</b-form-checkbox>
</b-form-checkbox-group>

Vue show modal click using data from JSON

I have JSON
"8":{
"name": "Dog",
"age": 2,
"showModal": false
},
"9":{
"name": "Cat",
"age": 7,
"showModal": false
},
Next I used v-for to show data:
<div v-for="(animal, index) in animals" :key="index">
<div>{{ animal.name }}</div> // Dog
<div>{{ animal.showModal }}</div> // false
<div #click="showModal(animal.showModal)">Show modal</div>
<div v-show="animal.showModal">modal...</div>
</div>
Method function:
showModal(showModal){
showModal = !showModal;
}
But function showModal not change value from false to true. How I can change this value to show modal ?
If you are loading from file you can try like this:
<div v-for="(animal, index) in animals" :key="index">
<div>{{ animal.name }}</div> // Dog
<div>{{ animal.showModal }}</div> // false
<div #click="showModal(index)">Show modal</div>
<div v-if="isItemClicked(index)">modal...</div>
</div>
In your data initialize indexes: [] and go and see if your index in that array.
methods: {
showModal(index) {
this.indexes.push(index);
},
isItemClicked(index) {
let test = false;
this.indexes.forEach(i => {
if (i === index) {
test = true;
break;
}
})
return test;
}
}
If you need to change data in your json you will need other procedure.
I think it was helpful. Good luck.
You have a typo in #clikc. Should be #click.
Tempalte:
<div #click="showModal(animal)">Show modal</div>.
Method:
showModal(animal) {
animal.showModal = true;
}

Computed properties in vue not reflected in select list

I have a vue component that is meant to create a select list with all available options. The save method puts the saved value into a vuex store. The available fields are generated using a computed property on the component that calls the vuex getter for the list.
In the component, there's a v-for with a v-if that checks that the select item isn't already being used by another component (by looking at a mapped property on the list item object).
Testing this, everything seems to be working as expected, the vuex store gets the list, it accepts the update, and once a save is called, the destination field is marked as mapped and that mapped property is visible in the vuex debug panel.
However, the other select lists on the page don't get updated to reflect the (now shorter) list of available options.
Once the select item is selected in another instance of the component, I'd expect the other components to drop that select option- but it appears the v-if is not re-evaluated after the initial load of the component?
Sorry, here's the basic component:
<template>
<div class="row">
<div class="col-4">
{{ item.source_id }}
</div>
<div class="col-8">
<select v-model="destination" class="form-control form-control-sm">
<option v-for="dest in destinationFields" v-if="shouldShow(dest)" v-bind:value="dest.id">{{ dest.id }} - {{ dest.label }} ({{ dest.dataType }})</option>
</select>
</div>
</div>
</template>
<script>
export default {
props: ['item'],
data() {
return {
destination: ''
}
},
methods: {
shouldShow: function(dest) {
if (this.hideDestination && (!dest.hasOwnProperty('mapped') || dest.id === this.destination)) {
return true
} else if (!this.hideDestination) {
return true
}
return false
}
},
computed: {
destinationFields: function() {
return this.$store.getters.visibleDestination
},
hideDestination: function() {
return this.$store.getters.hideMappedDestinations // boolean
}
}
}
I think a better approach would be to already filter the data inside of your computed function as follows:
computed: {
destinationFields: function() {
return this.$store.getters.visibleDestination()
.filter(dest => !dest.hasOwnProperty('mapped') || dest.id === this.destination)
},
hideDestination: function() {
return this.$store.getters.hideMappedDestinations // boolean
}
}
You would also have to change your template to:
<select v-model="destination" class="form-control form-control-sm">
<option v-for="dest in destinationFields" v-bind:value="dest.id">{{ dest.id }} - {{ dest.label }} ({{ dest.dataType }})</option>
</select>