How can I update the state of my vuex store? - vue.js

I'm new here so please forgive me if my write isn't good or the code section... still trying to figure it out.
so I've been trying a lot of forms in order to achieve the result that I want to but with no success...
Basically the app is getting a salary (amount) and there is the total sum in the account (currentSalar) both of them are from type string and not number... I need to find a way to rest the salary from the total sum and obviously update it.
Here are some screenshots If anyone could help me please.
**/index.js**
import mutations from './mutations.js';
import actions from './actions.js';
import getters from './getters.js';
export default {
namespaced: true,
state() {
return {
registers: [
{
id: 'user1',
currentSalar: '4,400' ,
saves: '12,000',
currency: '₪',
expense: [{salary: '3,000', category: 'Bar:'}]
},
{
id: 'user2',
currentSalar: '100,000',
saves: '3,000,000',
currency: '$',
expense: []
}
]
}
},
mutations,
actions,
getters
}
``````````````````````
``````````````````````
**/mutations.js**
export default {
addRegister(state, payload) {
const registerToUpdate = state.registers.find(
(register) => register.id === payload.id
);
const expenses = registerToUpdate.expense; //////* find((expense) => expense.salary === payload.salary) */
const salary = +expenses.salary;
let current = +registerToUpdate.currentSalar;
const newCurrnt = current -= salary; // should calculate a new value (UPDATE) for the currentSalar by resting the salary
console.log(newCurrnt);
expenses.unshift(payload);
},
};
```````````````````
**/actions.js**
export default {
addRegister(context, data) {
const RegisterData = {
id: context.rootGetters.userId,
category: data.category,
salary: data.salary
}
context.commit('addRegister', RegisterData);
}
}
```````````````````````
**/component**
<template>
<div class="container">
<h1>Add expense</h1>
<form #submit.prevent="ReturnCat">
<section>
<label for="salary">Amount: </label>
<input type="number" id="salary" v-model.number="salary"/>
</section>
<section>
<label for="category">Category: </label>
<select id="category" v-model.trim="category">
<option value="Bar: " id="bar">
Bar & Restaurants
</option>
<option value="Shopping: " id="shopping">Shopping</option>
<option value="Needs: " id="needs">Needs</option>
</select>
</section>
<base-button class="btn">ADD</base-button>
<h4>Add entery insted</h4>
</form>
</div>
</template>
<script>
export default {
props: ["id"],
data() {
return {
salary: null,
category: '',
};
},
computed: {
expensePath() {
return this.$router.replace("/register/" + this.id);
},
},
methods: {
ReturnCat() {
const form = {
salary: this.salary,
category: this.category
}
this.$store.dispatch('register/addRegister', form);
console.log(this.salary, this.category);
},
}
};
</script>
<style scoped>
#salary {
margin: 1rem auto auto 1rem;
height: 1.5rem;
width: 11rem;
}
select,
option {
margin: 1rem auto 2rem 1rem;
height: 1.5rem;
width: 11rem;
}
.btn {
margin: 0 auto;
width: 11rem;
}
</style>
`

JavaScript doesn't understand numbers as strings.
When you want to do arithmetic operations (as I guess you do, since you're developing an app having to do with accounting), you must work with numbers:
registers: [
{
id: "user1",
currentSalar: 4400,
saves: 12000,
currency: "₪",
expense: [{ salary: 3000, category: "Bar:" }],
},
{
id: "user2",
currentSalar: 100000,
saves: 3000000,
currency: "$",
expense: []
}
]
Even when you're getting this data from a poorly written API which returns numbers as strings, the first thing you want to do is to turn them into numbers, before placing them in the store.
And when you try to cast a string to number (by appending + to it), if the string contains thousand separators as commas (or anything that's not a digit, a dot, or a scientific number notation - e.g: '-3e6'), you're going to get back NaN.
See it here:
const num = '1,000,000'
console.log( +num ) // NaN
// strip commas from it:
console.log( +num.replaceAll(',', '') ) // 1000000
And when you want to display the numbers used in the store to the user, if you want to put thousand and decimal separators on them, use Intl.NumberFormat.

solved:)
here is the code:
// index.js
import mutations from './mutations.js';
import actions from './actions.js';
import getters from './getters.js';
export default {
namespaced: true,
state() {
return {
registers: [
{
id: 'user1',
currentSalar: 4400 ,
saves: 12000 ,
currency: '₪',
expense: []
},
{
id: 'user2',
currentSalar: 100000,
saves: 3000000,
currency: '$',
expense: []
}
]
}
},
mutations,
actions,
getters
}`
`
// mutations.js
export default {
addRegister(state, payload) {
const registerToUpdate = state.registers.find(
(register) => register.id === payload.id
);
let current = registerToUpdate.currentSalar;
const expenses = registerToUpdate.expense;
current -= payload.salary;
expenses.unshift(payload);
registerToUpdate.currentSalar = registerToUpdate.currentSalar -
payload.salary;
},
};

Related

Property 'campaign' was accessed during render but is not defined on instance

These are the issues i'm getting
Here below is the code that produces the problems, this part in particular:
When ever i;m trying to filter campaigns using company_id and product_id with v-if the problem occurs. Almost the same exact code works a few lines above filtering products. I have no idea what to do next. I tried refs and putting the mocked that into reactive variable and computeing it with a function but it didn;t work out.
<script setup>
import CompanyItem from "./CompanyItem.vue";
import ProductItem from "./ProductItem.vue";
import CampaignItem from "./CampaignItem.vue";
import { useCurrentCompanyStore } from "../stores/currentCompanyStore.js"
import { useCurrentProductStore } from "../stores/currentProductStore.js"
const companyStore = useCurrentCompanyStore();
const productStore = useCurrentProductStore();
const companies =
[
{
company_id: 1,
name: 'Domain of Man',
fund_balance: 100000,
products_list: [
{
product_id: 1,
name: 'gate'
},
{
product_id: 2,
name: 'exploration ship'
},
{
product_id: 3,
name: 'artifacts'
}
]
},
{
company_id: 2,
name: 'Hegemony',
fund_balance: 200000,
products_list: [
{
product_id: 1,
name: 'toothbrash'
},
{
product_id: 2,
name: 'ore'
},
{
product_id: 3,
name: 'food'
}
]
},
];
const campaigns = [
{
campaign_id: 1,
company_id: 1,
product_id: 1,
campaign_name: "Gates for everyone",
keywords: [
"one for each",
"limited offer"
],
bid_amount: 25000,
status: true,
town: "Tarnow",
radius: "10"
},
{
campaign_id: 2,
company_id: 1,
product_id: 3,
campaign_name: "Get them while they last",
keywords: [
"rare",
"one for each",
"limited offer"
],
bid_amount: 25000,
status: false,
town: "Tarnow",
radius: "10"
},
{
campaign_id: 3,
company_id: 3,
product_id: 1,
campaign_name: "Let the shine power your ship",
keywords: [
"electricity",
"green technology",
],
bid_amount: 25000,
status: true,
town: "Tarnow",
radius: "10"
}
];
</script>
<template>
<div class="container">
<div class="companies" >
<CompanyItem v-for="company in companies" v-bind:key="company.company_id" :company-id="company.company_id">
<template #name>
{{ company.name }}
</template>
<template #budget>
{{ company.fund_balance }}
</template>
</CompanyItem>
</div>
<div class="products">
<template v-for="company in companies">
<ProductItem
v-for="product in company.products_list"
v-bind:key="product.product_id"
:id="company.company_id"
v-if="companyStore.companyId === company.company_id"
:product-id="product.product_id">
<template #name>
{{ product.name }}
</template>
</ProductItem>
</template>
</div>
<div class="campaigns">
<CampaignItem
v-for="campaign in campaigns"
v-if="companyStore.companyId === campaign.company_id"
v-bind:key="campaign.campaign_id"
:id="campaign.campaign_id"
>
<template #name>
{{campaign.campaign_name}}
</template>
</CampaignItem>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto;
grid-template-areas:
"companies products campaigns";
}
.companies {
grid-area: companies;
display: flex;
flex-direction: column;
overflow: hidden;
}
.products {
grid-area: products;
}
.campaigns {
grid-area: campaigns;
}
</style>
Here are stores:
import { defineStore } from 'pinia'
export const useCurrentCompanyStore = defineStore({
id: 'currentComapny',
state: () => ({
companyId: -1
}),
getters: {
getCompanyId: (state) => state.companyId
},
actions: {
change(newCompanyId) {
this.companyId = newCompanyId;
}
}
})
import { defineStore } from 'pinia'
export const useCurrentProductStore = defineStore({
id: 'currentProduct',
state: () => ({
productId: -1
}),
getters: {
getCompanyId: (state) => state.productId
},
actions: {
change(newProductId) {
this.productId = newProductId;
}
}
})
Btw. if anybody wants to run it themself here is the git repo, its feature/frontend branch:
https://github.com/kuborek2/campaign_planer
You must not use v-if and v-for on the same element because v-if will always be evaluated first due to implicit precedence.
And exactly because of that, you are facing this error of undefined company_id as v-for is not executed yet and v-if is trying to access it.
Make the changes as suggested below and it should fix your error.
<CampaignItem
v-for="campaign in campaigns"
:key="campaign.campaign_id"
:id="campaign.campaign_id"
>
<template v-if="companyStore.companyId === campaign.company_id" #name>
{{campaign.campaign_name}}
</template>
</CampaignItem>
Click here for the reference

Vue: Update part of an html string from data v-for loop with v-model input

Using Vue2 I have an array of objects in data which have an html string rendered in a v-for loop. Part of each string is a prop, which renders correctly initially. However, when the prop value is updated with v-model the data in the v-for loop is not updated.
jsfiddle: When the input is changed from "Bob" to "Sally" all instances should change, but those in the for-loop do not.
html
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="statement in statements" v-html="statement.text"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
vue
new Vue({
el: "#app",
data: function() {
return {
statements: [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Running is the favorite activity of <b>'+ this.name + '</b>',
},
],
}
},
props: {
name: {
type: String,
default: 'Bob',
},
},
})
The code has been simplified - the actual HTML strings have ~3 variables each that need to update, and are at different locations in each string, so I can't think of another way to replace the values when they are updated, while preserving the html tags. This is intended to be a single-page vue application, but is using Laravel and blade for some general page formatting.
name should be in data, not in props (it's not coming from a parent, it's just reactive data, which needs to be tracked for changes internally - inside this component).
statements should be in computed, because you want Vue to update it whenever its reactive references (e.g: this.name) change. Besides, this is not what you think it is inside the data function.
See it working:
new Vue({
el: "#app",
data: () => ({
name: 'Bob'
}),
computed: {
statements() {
return [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Runing is the favorite activity of <b>'+ this.name + '</b>',
},
]
}
},
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
p.italic {
font-style: italic;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text" :key="key"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
If you're trying to create a reusable component which takes in a person (with some values) and creates the statements based on those values and also allows editing the person's values, here's how to do it:
Vue.component('person-editor', {
template: '#person-editor-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
details: [
{ name: 'name', placeholder: 'Name' },
{ name: 'fun', placeholder: 'Running', label: 'Favorite activity' },
{ name: 'food', placeholder: 'pizza', label: 'Favorite food'}
]
}),
methods: {
update(payload) {
this.$emit('update:person', { ...this.person, ...payload })
},
getDetailLabel(d) {
return d.label || (d.name[0].toUpperCase() + d.name.slice(1))
}
}
});
Vue.component('person-details', {
template: '#person-details-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
statements: [
{ id: 'food', text: p => `<b>${p.name}</b> likes to eat ${p.food}.` },
{ id: 'fun', text: p => `${p.fun} is the favorite activity of <b>${p.name}</b>` }
]
})
})
new Vue({
el: "#app",
data: () => ({
persons: [
{ name: 'Jane', food: 'apples', fun: 'Hiking' },
{ name: 'Jack', food: 'pizza', fun: 'Sleeping' }
]
}),
methods: {
updatePerson(key, value) {
this.persons.splice(key, 1, value);
}
}
})
label {
display: flex;
}
label > span {
width: 150px;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<template v-for="(person, key) in persons">
<hr v-if="key" :key="`hr-${key}`">
<person-details :person="person"
:key="`details-${key}`"
></person-details>
<person-editor :person.sync="person"
:key="`editor-${key}`"
#update:person="updatePerson(key, person)"></person-editor>
</template>
</div>
<template id="person-editor-tpl">
<div>
<template v-for="detail in details">
<label :key="detail.name">
<span v-text="getDetailLabel(detail)"></span>
<input :value="person[detail.name]"
#input="e => update({ [detail.name]: e.target.value })">
</label>
</template>
</div>
</template>
<template id="person-details-tpl">
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text(person)"></li>
</ul>
</template>
I separated the editor and the display in two separate components.
Because I had to define the components on the Vue instance it's a bit crammed in this example, but it looks a lot more elegant when using sfcs (and each component is a standalone .vue file).

Vue.js 3 - How can I pass data between Vue components and let both views also update?

I tried the following.
Please note the commented line in parent.vue that doesn't even commit the new state for me.
However maybe someone can guide me to a better solution for a global state shared by multiple components?
main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from 'vuex'
const app = createApp(App);
export const store = createStore({
state: {
textProp: 'test',
count: 1
},
mutations: {
setState(state, newState) {
console.log('setState');
state = newState;
}
},
getters: {
getAll: (state) => () => {
return state;
}
}
});
app.use(store);
app.mount('#app')
parent.vue
<template>
<div class="parent">
<div class="seperator" v-bind:key="item" v-for="item in items">
<child></child>
</div>
<button #click="toonAlert()">{{ btnText }}</button>
<button #click="veranderChild()">Verander child</button>
</div>
</template>
<script>
import child from "./child.vue";
import {store} from '../main';
export default {
name: "parent",
components: {
child,
},
store,
data: function () {
return {
items: [
{
id: 1,
valueText: "",
valueNumber: 0,
},
{
id: 2,
valueText: "",
valueNumber: 0,
},
{
id: 3,
valueText: "",
valueNumber: 0,
},
],
};
},
props: {
btnText: String,
},
methods: {
toonAlert() {
alert(JSON.stringify(this.$store.getters.getAll()));
},
veranderChild() {
console.log('child aan het veranderen (parentEvent)');
this.$store.commit('setState', { // This is especially not working.
textProp: 'gezet via de parent',
count: 99
})
this.$store.commit({type: 'setState'}, {
'textProp': 'gezet via de parent',
'count': 99
});
},
},
};
</script>
<style>
.seperator {
margin-bottom: 20px;
}
.parent {
/* background: lightblue; */
}
</style>
child.vue
<template>
<div class="child">
<div class="inputDiv">
text
<input #change="update" v-model="deText" type="text" name="deText" />
</div>
<div class="inputDiv">
nummer
<input v-model="hetNummer" type="number" name="hetNummer" />
</div>
<button #click="toonState">Toon huidige state</button>
</div>
</template>
<script>
import {store} from '../main';
export default {
name: "child",
store,
data: function() {
return {
'hetNummer': 0
}
},
methods: {
update(e) {
let newState = this.$store.state;
newState.textProp = e.target.value;
// this.$store.commit('setState', newState);
},
toonState()
{
console.log( this.$store.getters.getAll());
}
},
computed: {
deText: function() {
return '';
// return this.$store.getters.getAll().textProp;
}
}
};
</script>
<style>
.inputDiv {
float: right;
margin-bottom: 10px;
}
.child {
max-width: 300px;
height: 30px;
margin-bottom: 20px;
/* background: yellow; */
margin: 10px;
}
</style>
You have a misconception about JavaScript unrelated to Vue/Vuex. This doesn't do what you expect:
state = newState;
Solution (TL;DR)
setState(state, newState) {
Object.assign(state, newState);
}
Instead of setting the state variable, merge the new properties in.
Explanation
Object variables in JavaScript are references. That's why if you have multiple variables referring to the same object, and you change a property on one, they all mutate. They're all just referring to the same object in memory, they're not clones.
The state variable above starts as a reference to Vuex's state object, which you know. Therefore, when you change properties of it, you mutate Vuex's state properties too. That's all good.
But when you change the whole variable-- not just a property-- to something else, it does not mutate the original referred object (i.e. Vuex's state). It just breaks the reference link and creates a new one to the newState object. So Vuex state doesn't change at all. Here's a simpler demo.
Opinion
Avoid this pattern and create an object property on state instead. Then you can just do state.obj = newState.
You should use a spread operator ... to mutate your state as follows :
state = { ...state, ...newState };
LIVE EXAMPLE
but I recommend to make your store more organized in semantic way, each property in your state should have its own setter and action, the getters are the equivalent of computed properties in options API they could be based on multiple state properties.
export const store = createStore({
state: {
count: 1
},
mutations: {
SET_COUNT(state, _count) {
console.log("setState");
state.count=_count
},
INC_COUNT(state) {
state.count++
}
},
getters: {
doubleCount: (state) => () => {
return state.count*2;
}
}
});
**Note : ** no need to import the store from main.js in each child because it's available using this.$store in options api, but if you're working with composition api you could use useStore as follows :
import {useStore} from 'vuex'
setup(){
const store=useStore()// store instead of `$store`
}

Google Maps showing grey box in Vue modal

I have a <b-modal> from VueBootstrap, inside of which I'm trying to render a <GmapMap> (https://www.npmjs.com/package/gmap-vue)
It's rendering a grey box inside the modal, but outside the modal it renders the map just fine.
All the searching I've done leads to the same solution which I'm finding in some places is google.maps.event.trigger(map, 'resize') which is not working. Apparently, it's no longer part of the API [Source: https://stackoverflow.com/questions/13059034/how-to-use-google-maps-event-triggermap-resize]
<template>
<div class="text-center">
<h1>{{ title }}</h1>
<div class="row d-flex justify-content-center">
<div class="col-md-8">
<GmapMap
ref="topMapRef"
class="gmap"
:center="{ lat: 42, lng: 42 }"
:zoom="7"
map-type-id="terrain"
/>
<b-table
bordered
dark
fixed
hover
show-empty
striped
:busy.sync="isBusy"
:items="items"
:fields="fields"
>
<template v-slot:cell(actions)="row">
<b-button
size="sm"
#click="info(row.item, row.index, $event.target)"
>
Map
</b-button>
</template>
</b-table>
<b-modal
:id="mapModal.id"
:title="mapModal.title"
#hide="resetInfoModal"
ok-only
>
<GmapMap
ref="modalMapRef"
class="gmap"
:center="{ lat: 42, lng: 42 }"
:zoom="7"
map-type-id="terrain"
/>
</b-modal>
</div>
</div>
</div>
</template>
<script>
// import axios from "axios";
import { gmapApi } from 'gmap-vue';
export default {
name: "RenderList",
props: {
title: String,
},
computed: {
google() {
return gmapApi();
},
},
updated() {
console.log(this.$refs.modalMapRef);
console.log(window.google.maps);
this.$refs.modalMapRef.$mapPromise.then((map) => {
map.setCenter(new window.google.maps.LatLng(54, -2));
map.setZoom(2);
window.google.maps.event.trigger(map, 'resize');
})
},
data: function () {
return {
items: [
{ id: 1, lat: 42, long: 42 },
{ id: 2, lat: 42, long: 42 },
{ id: 3, lat: 42, long: 42 },
],
isBusy: false,
fields: [
{
key: "id",
sortable: true,
class: "text-left",
},
{
key: "text",
sortable: true,
class: "text-left",
},
"lat",
"long",
{
key: "actions",
label: "Actions"
}
],
mapModal: {
id: "map-modal",
title: "",
item: ""
}
}
},
methods: {
// dataProvider() {
// this.isBusy = true;
// let promise = axios.get(process.env.VUE_APP_LIST_DATA_SERVICE);
// return promise.then((response) => {
// this.isBusy = false
// return response.data;
// }).catch(error => {
// this.isBusy = false;
// console.log(error);
// return [];
// })
// },
info(item, index, button) {
this.mapModal.title = `Label: ${item.id}`;
this.mapModal.item = item;
this.$root.$emit("bv::show::modal", this.mapModal.id, button);
},
resetInfoModal() {
this.mapModal.title = "";
this.mapModal.content = "";
},
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
margin-bottom: 60px;
}
.gmap {
width: 100%;
height: 300px;
margin-bottom: 60px;
}
</style>
Does anyone know how to get the map to display properly in the modal?
Surely, I'm not the first to try this?
Had this problem, in my case it was solved by providing the following options to google maps:
mapOptions: {
center: { lat: 10.365365, lng: -66.96667 },
clickableIcons: false,
streetViewControl: false,
panControlOptions: false,
gestureHandling: 'cooperative',
mapTypeControl: false,
zoomControlOptions: {
style: 'SMALL'
},
zoom: 14
}
However you can probably make-do with just center and zoom.
Edit: Try using your own google maps components, follow this tutorial:
https://v2.vuejs.org/v2/cookbook/practical-use-of-scoped-slots.html#Base-Example
You can use the package described in the tutorial to load the map, dont be scared by the big red "deprecated" warning on the npm package page.
However for production, you should use the package referenced by the author, which is the one backed by google:
https://googlemaps.github.io/js-api-loader/index.html
The only big difference between the two:
The 'google' object is not returned by the non-deprecated loader, it is instead attached to the window. See my answer here for clarification:
'google' is not defined Using Google Maps JavaScript API Loader
Happy coding!

Keeping track of an array of components

I have a really simple Vue app:
<div id="app">
<item v-for="item in items" v-bind:title="item.title" v-bind:price="item.price"
#added="updateTotal(item)"></item>
<total v-bind:total="total"></total>
</div>
And a Vue instance:
Vue.component('item',{
'props' : ['title', 'price'],
'template' : "<div class='item'><div>{{ title }} – ${{total}} </div><button class='button' #click='add'>Add</button></div>",
'data' : function(){
return {
quantity : 0
}
},
'computed' : {
total : function(){
return (this.quantity * this.price).toFixed(2);
}
},
methods : {
add : function(){
this.quantity ++;
this.$emit('added');
}
}
});
Vue.component('total', {
'props' : ['total'],
'template' : "<div class='total'>Total: ${{ total }}</div>",
});
var app = new Vue({
'el' : '#app',
'data' : {
'total' : 0,
'items': [
{
'title': 'Item 1',
'price': 21
}, {
'title': 'Item 2',
'price': 7
}
],
},
methods : {
'updateTotal' : function(item){
console.log('updating');
this.total += item.price;
}
}
});
Demo link:
https://codepen.io/EightArmsHQ/pen/rmezQq?editors=1010
And what I'd like to do is update the <total> component as the various items are added to the cart. I have it working at the moment, however it doesn't seem very elegant.
Right now, I add the price of each item to a total. What I'd really like to do is have the total as a computed property, and then every time an item component is changed, loop through them all adding the quantity * price of each. Is there a way I can do this?
One option I have come up with just now is replacing my updateTotal method in the main app to the below:
methods : {
'updateTotal' : function(item){
item.quantity += 1;
}
},
computed : { total : function(){
var t = 0;
for(var i = 0; i < this.items.length; i ++){
t += this.items[i].quantity * this.items[i].price;
}
return t;
}
}
So, beginning to store the quantity of each item inside the Vue app, not the component. But it makes more sense to store the quantity of each item inside its own component... doesn't it? What is the best way of handling this?
Maybe counter-intuitively, the components only need their data as props. The items (as data objects) are defined in the parent; just define quantity there, too. Then use those data items in the components, but make changes via events to the parent.
With an array that includes the quantities, it's easy to create the computed total you want.
Vue.component('item', {
'props': ['item'],
'template': "<div class='item'><div>{{ item.title }} – ${{total}} </div><button class='button' #click='add'>Add</button></div>",
'computed': {
total: function() {
return (this.item.quantity * this.item.price).toFixed(2);
}
},
methods: {
add: function() {
this.$emit('added');
}
}
});
Vue.component('total', {
'props': ['total'],
'template': "<div class='total'>Total: ${{ total }}</div>",
});
var app = new Vue({
'el': '#app',
'data': {
'items': [{
'title': 'Item 1',
'price': 21,
'quantity': 0
}, {
'title': 'Item 2',
'price': 7,
'quantity': 0
}],
},
computed: {
total() {
return this.items.reduce((a, b) => a + (b.price * b.quantity), 0).toFixed(2);
}
},
methods: {
updateTotal(item) {
++item.quantity;
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<item v-for="item in items" v-bind:item="item" #added="updateTotal(item)"></item>
<total v-bind:total="total"></total>
</div>