Iam starting with this hobby in VUE3 :) I try summary item books.price from object Object is in parent component and I want make this in child components. I have object in App.vue:
<script>
import BooksList from './components/BooksList.vue'
import BooksLengthMsg from './components/BooksLengthMsg.vue'
import booksSummary from './components/BookSummary.vue'
// import { computed } from 'vue'
export default {
components: { BooksList, BooksLengthMsg, booksSummary },
name: 'App',
data: () => ({
books: [
{
title: 'Windows Powershell w miesiąc',
price: 20
},
{
title: 'Alicja w krainie czarów',
price: 18
}
],
form: {
title: '',
price: null
}
}),
methods: {
removeBook (index) {
this.books.splice(index, 1)
console.log('delete', index)
},
handleSubmit () {
const newBook = { ...this.form }
this.books.push(newBook)
this.resetForm()
},
resetForm () {
this.form.price = null
this.form.title = ''
}
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div id="app">
<header>
<h1>Books<span>.app</span></h1>
</header>
<books-list #remove="removeBook" :books="books" />
<books-length-msg :books="books"/>
<form #submit.prevent="handleSubmit">
<label>
Title:
<input v-model="form.title" type="text" name="title">
</label>
<label>
Price:
<input v-model="form.price" type="number" name="price">
</label>
<button type="submit">Add book</button>
</form>
</div>
<books-summary :books="books" />
</template>
and I maked new component BookSummary and I run Books Amount but I cant run function for total price, I cant save in const priceSummary.
<script>
import { ref } from 'vue'
export default {
name: 'BooksSummary',
setup () {
const priceSummary = ref(0)
return { priceSummary }
},
props: {
books: {
type: Array,
required: true
}
},
computed: {
bookAmount () {
return this.books.length
},
totalPrice () {
// const totalPr = this.priceSummary
// return this.books.forEach((book) => book.price)
return this.books.forEach((book) => { console.log(book.price) })
}
}
}
</script>
<template>
<div>Books Amount : {{ bookAmount }}</div>
<div>Total price : const priceSummary:{{ priceSummary}} function {{ totalPrice }}</div>
</template>
<style>
div {
padding: 5px;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
It was stupid facil error I change { this.priceSummary += book.price }), but now I must make it with reduce (thx for answer) and make parseInt.
<script>
import { ref } from 'vue'
export default {
name: 'BooksSummary',
setup () {
const priceSummary = ref(0)
return { priceSummary }
},
props: {
books: {
type: Array,
required: true
}
},
computed: {
bookAmount () {
return this.books.length
},
totalPrice () {
return this.books.forEach((book) => { this.priceSummary += book.price })
}
}
}
</script>
<template>
<div>Books Amount : {{ bookAmount }}</div>
<div>Total price : {{ priceSummary}} {{ totalPrice }}</div>
</template>
<style>
div {
padding: 5px;
}
</style>
You need to change your totalPrice computed property to:
totalPrice () {
this.priceSummary = this.books.reduce((sum, book) => sum + book.price, 0)
return this.priceSummary;
}
Using .reduce is fairly standard and performant way to get a total from array
Here is reference for the reduce function.
Related
I use vue js version 2.6.11
I have 3 component vue
My first component like this :
<template>
...
<page-listing
:lastPage="lastPage"
:perPage="perPage"
:changePage="changePage"
></page-listing>
</template>
...
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import PageListing from "#/views/app/report/PageListing";
export default {
components: {
"page-listing": PageListing
},
methods: {
...mapActions([
"getReport",
"setPageAction",
]),
searchChange() {
this.loadPage()
},
async changePage(pageNum) {
await this.setPageAction(pageNum)
this.loadPage();
},
loadPage(){
const page = this.page
this.getReport({page});
},
},
computed: {
...mapGetters({
perPage: "reportPerpage",
lastPage: "reportLastPage",
page: "reportPage",
}),
},
mounted() {
this.loadPage();
}
};
</script>
My second component like this :
<template>
...
<div class="card-body">
<p class="mb-0 w-5 w-sm-100">Number</p>
<div class="w-30 w-sm-100">Description</div>
<div class="w-20 w-sm-100">Date</div>
<div class="w-10 w-sm-100">Modified By</div>
</div>
...
<b-row key="list">
<b-colxx xxs="12" v-for="(item,index) in itemsWithLineNumber" :key="index" :id="item.id">
<list-item
:key="item.id"
:data="item"
/>
</b-colxx>
</b-row>
<b-row v-if="lastPage>1">
...
<b-pagination-nav
:number-of-pages="lastPage"
:link-gen="linkGen"
:value="page"
#change="(a)=>changePage(a)"
:per-page="perPage"
align="center"
use-router
>
...
</b-row>
...
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import ListItem from "./ListItem";
export default {
components: {
"list-item": ListItem
},
props: [
"lastPage",
"changePage",
],
methods: {
...mapActions(["setItemsAction"]),
linkGen(pageNum) {
return pageNum === 1 ? '?' : `?page=${pageNum}`
},
},
computed: {
...mapGetters({
perPage: "reportPerpage",
page: "reportPage",
items: "reportItems",
}),
filtered() {
const start = this.page * this.perPage - this.perPage
return this.items.slice(start, start + this.perPage)
},
itemsWithLineNumber() {
return this.filtered.map((item, idx) => {
return {...item, lineNumber: (this.page - 1) * this.perPage + idx + 1}
})
}
}
};
</script>
My three component like this :
<template>
<b-card no-body>
<div class="pl-2 d-flex">
<div class="card-body">
<p class="mb-0 text-muted w-5">{{data.lineNumber}}</p>
<p class="mb-0 text-muted w-30">{{data.description}}</p>
<p class="mb-0 text-muted w-20">{{data.date}}</p>
<p class="mb-0 text-muted w-10">{{data.created_by}}</p>
</div>
</div>
</b-card>
</template>
<script>
export default {
props: ['data'],
}
</script>
My vuex store like this :
const state = {
items: null,
reportError: '',
reportSuccess: '',
page: 1,
perPage: 4,
lastPage: 0,
}
const getters = {
reportError: state => state.reportError,
reportSuccess: state => state.reportSuccess,
reportItems: state => state.items,
reportPage: state => state.page,
reportPerpage: state => state.perPage,
reportLastPage: state => state.lastPage,
}
const mutations = {
getReportSuccess (state, res) {
state.items = res.data
state.perPage = res.meta.per_page;
state.lastPage = res.meta.last_page;
},
getReportError (state, error) {
state.items = null
},
setReportPage (state, payload) {
state.page = payload
},
setPageMutation (state, payload) {
state.page = payload
},
setItemsMutation (state, payload) {
state.items = payload
},
}
const actions = {
getReport ({ commit }, payload) {
...
axios
.get(`${api}/report/list`, { params })
.then(r => r.data)
.then(res => {
if (res.data) {
commit('getReportSuccess', res.data)
} else {
commit('getReportError', 'error:getReport')
}
})
},
setReportPage ({ commit }, payload) {
commit('setReportPage', payload)
},
setPageAction({ commit}, payload) {
commit('setPageMutation', payload)
},
setItemsAction({ commit}, payload) {
commit('setItemsMutation', payload)
},
}
export default {
state,
getters,
mutations,
actions,
}
When the page loads the first time, the line number works and appears. But when I click on the page 2, the page displays blank data
How can I solve this problem?
Please help. Thanks
Update :
Demo like this : https://codesandbox.io/s/prod-hooks-dzk07b
Your problem is with the filtered
filtered() {
const start = this.currentPage * this.perPage - this.perPage;
return this.items.slice(start, start + this.perPage);
},
the items response only has the first n results, when you get rid of them using filter, the array is empty.
you can remove the function and use items result instead
itemsWithLineNumber() {
return this.items.map((item, idx) => {
return {
...item,
lineNumber: (this.currentPage - 1) * this.perPage + idx + 1,
};
});
},
I have this component that I'm using as popup:
<template>
<full-screen-popup ref="popup">
<div class="confirm-dialogue-container">
<div class="dialog-header">
<h1 id="confirm-title-lbl">{{ title }}</h1>
<h2 id="confirm-message-lbl">{{ message }}</h2>
</div>
<div class="actions-container">
<button id="bt-confirm-delete" class="btn-action primary" #click="_confirm">{{ confirmLblButton }}</button>
<button id="bt-cancel-delete" class="btn-action " #click="_cancel">{{ cancelLblButton }}</button>
</div>
</div>
</full-screen-popup>
<script>
import FullScreenPopup from './FullScreenPopup.vue'
export default {
name: 'ReplaceDialogue',
components: { FullScreenPopup },
data: () => ({
title: undefined,
message: undefined,
confirmLblButton: undefined,
cancelLblButton: undefined,
resolvePromise: undefined,
rejectPromise: undefined
}),
methods: {
show (opts = {}) {
this.title = opts.title
this.message = opts.message
this.confirmLblButton = opts.confirmLblButton
if (opts.cancelLblButton) {
this.cancelLblButton = opts.cancelLblButton
}
this.$refs.popup.open()
// Return promise so the caller can get results
return new Promise((resolve, reject) => {
this.resolvePromise = resolve
this.rejectPromise = reject
})
},
_confirm () {
this.$refs.popup.close()
this.resolvePromise(true)
},
_cancel () {
this.$refs.popup.close()
this.resolvePromise(false)
}
}
}
</script>
<template>
<transition name="fade">
<div class="full-screen-popup" v-if="isVisible">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: 'FullScreenPopup',
data: () => ({
isVisible: false
}),
methods: {
open () {
this.isVisible = true
},
close () {
this.isVisible = false
}
}
}
</script>
Now, I would like to write some unit tests about for this component:
it('Test', () => {
const wrapper = mount(ReplaceDialogue)
const promise = wrapper.vm.show({
title: popupTitle,
message: popupMessage,
confirmLblButton: confirmButton,
cancelLblButton: cancelButton
})
let x = wrapper.find('#confirm-title-lbl')
})
Seams that the html is empty. I'm not able to found any element with the wrapper.find(..)
Any suggestions?
I have 2 inputs in which i provide value to search whether its name of the company, position (1st input) or location (2nd input). It works with one argument provided into foundJobs mutation and then into action. But when payload has an object everything is undefined and array is empty. What am i doing wrong?
component:
<script setup>
import IconSearch from "../Icons/icon-search.vue";
import IconLocation from "../Icons/icon-location.vue";
import { ref } from "vue";
import { useStore } from "vuex";
const store = useStore();
const nameFilter = ref("");
const locationFilter = ref("");
</script>
<template>
<div class="header-filter">
<div class="header-filter__search">
<IconSearch />
<input
type="text"
placeholder="Filter by title, companies, expertise…"
ref="nameFilter"
/>
</div>
<div class="header-filter__location">
<IconLocation />
<input
type="text"
placeholder="Filter by location…"
ref="locationFilter"
/>
</div>
<div class="header-filter__fulltime">
<input type="checkbox" />
<p>Full Time Only</p>
<button
type="button"
#click="
store.dispatch('foundJobs', {
nameFilter: nameFilter.value,
locationFilter: locationFilter.value,
})
"
>
Search
</button>
</div>
</div>
</template>
vuex: (not working)
import { createStore } from "vuex";
const store = createStore({
state() {
return {
jobs: [],
filteredJobs: [],
};
},
mutations: {
setJobs(state, jobs) {
state.jobs = jobs;
},
foundJobs(state, { nameInputValue, locationInputValue }) {
let copiedJobsArr = [...state.jobs];
if (nameInputValue !== "") {
copiedJobsArr = copiedJobsArr.filter(
(job) =>
job.company === nameInputValue || job.position === nameInputValue
);
}
if (locationInputValue !== "") {
copiedJobsArr = copiedJobsArr.filter(
(job) => job.location === locationInputValue
);
}
console.log(locationInputValue); // undefined
state.filteredJobs = copiedJobsArr;
console.log(state.filteredJobs); //empty array
},
},
actions: {
foundJobs(context, { nameInputValue, locationInputValue }) {
context.commit("foundJobs", { nameInputValue, locationInputValue });
},
loadJobs(context) {
return fetch("./data.json")
.then((response) => {
return response.json();
})
.then((data) => {
const transformedData = data.map((job) => {
return {
id: job.id,
company: job.company,
logo: job.logo,
logoBackground: job.logoBackground,
position: job.position,
postedAt: job.postedAt,
contract: job.contract,
location: job.location,
website: job.website,
apply: job.apply,
description: job.description,
reqContent: job.requirements.content,
reqItems: job.requirements.items,
roleContent: job.role.content,
roleItems: job.role.items,
};
});
context.commit("setJobs", transformedData);
});
},
},
getters: {
jobs(state) {
return state.jobs;
},
filteredJobOffers(state) {
return state.filteredJobs;
},
},
});
export default store;
vuex (working) - here i also provide one argument into action assigned to a button (in a component file)
import { createStore } from "vuex";
const store = createStore({
state() {
return {
jobs: [],
filteredJobs: [],
};
},
mutations: {
setJobs(state, jobs) {
state.jobs = jobs;
},
foundJobs(state, nameInputValue) {
let copiedJobsArr = [...state.jobs];
if (nameInputValue !== "") {
copiedJobsArr = copiedJobsArr.filter(
(job) =>
job.company === nameInputValue || job.position === nameInputValue
);
}
console.log(nameInputValue);
state.filteredJobs = copiedJobsArr;
console.log(state.filteredJobs);
},
},
actions: {
foundJobs(context, nameInputValue) {
context.commit("foundJobs", nameInputValue);
},
loadJobs(context) {
return fetch("./data.json")
.then((response) => {
return response.json();
})
.then((data) => {
const transformedData = data.map((job) => {
return {
id: job.id,
company: job.company,
logo: job.logo,
logoBackground: job.logoBackground,
position: job.position,
postedAt: job.postedAt,
contract: job.contract,
location: job.location,
website: job.website,
apply: job.apply,
description: job.description,
reqContent: job.requirements.content,
reqItems: job.requirements.items,
roleContent: job.role.content,
roleItems: job.role.items,
};
});
context.commit("setJobs", transformedData);
});
},
},
getters: {
jobs(state) {
return state.jobs;
},
filteredJobOffers(state) {
return state.filteredJobs;
},
},
});
export default store;
store.dispatch('foundJobs', {
nameFilter: nameFilter.value,
locationFilter: locationFilter.value,
})
You are sending data like this and trying to get on the wrong way
foundJobs(state, { nameInputValue, locationInputValue })
you can receive data this way:
foundJobs(state, { nameFilter, locationFilter})
I need to render a map using Mapbox only when data is ready.
I have the following code in my Vuex store:
/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
In my App.vue I fetch all APIs requests using an async created method.
App.vue:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
async created() {
await this.$store.dispatch("FETCH_ACTIVITY");
await this.$store.dispatch("FETCH_GEO_IPS");
},
};
</script>
In my Dashboard component I have a conditional rendering to draw the maps component only when geoIps.length > 0
Dashboard.vue:
<template>
<div v-if="geoIps.length > 0">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
geoIps() {
return this.$store.state.geoIps;
},
};
</script>
Then I load the Maps component.
<template>
<q-card class="bg-primary APP__card APP__card-highlight">
<q-card-section class="no-padding no-margin">
<div id="map"></div>
</q-card-section>
</q-card>
</template>
<script>
import "mapbox-gl/dist/mapbox-gl.css";
import mapboxgl from "mapbox-gl/dist/mapbox-gl";
export default {
name: "maps-geo-ips-card",
computed: {
geoIps() {
return this.$store.state.geoIps;
},
},
created() {
mapboxgl.accessToken = process.env.MAPBOX_API_KEY;
},
mounted() {
const mapbox = new mapboxgl.Map({
container: "map",
center: [0, 15],
zoom: 1,
});
this.geoIps.map((geoIp) =>
new mapboxgl.Marker()
.setLngLat([geoIp.longitude, geoIp.latitude])
.addTo(mapbox)
);
},
};
</script>
<style>
#map {
height: 500px;
width: 100%;
border-radius: 25px;
overflow: hidden;
}
</style>
The problem is that when the function resolves the first IP address, the map is drawn showing only one address and not all the others like this:
What is the best way to only draw the map when my FETCH_GEO_IPS function has finished?
Thanks in advance
I think the answer lies in this bit of code:
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
Your map function loops through every element of the array and commits each IP one by one. So when the first one is committed, your v-if="geoIps.length > 0" is true.
A workaround would be to set a flag only when the IPs are set.
This is a proposed solution:
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
isReady: false
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
setIsReady: (state, value) => {
state.isReady = value;
}
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
let tofetch = getters.activityIps.length; // get the number of fetch to do
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
toFetch -= 1; // decrement after each commit
if (toFetch === 0) {
commit("setIsReady", true); // all commits are done
}
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
And in your view:
<template>
<div v-if="isReady">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
isReady() {
return this.$store.state.isReady;
},
};
</script>
Background:
I have a child component that receives an array called expenseButton as props. Within the array are objects with values which I am trying to get the sum of using array.reduce()
Problem
When I use methods to get the sum of the values it works perfectly fine but when I try to make it a computed property I get an error that states:
("test" is the name of the computed property)
Property or method "test" is not defined on the instance but referenced during render
<script>
export default {
props: {
expenseButton: Array,
},
data() {
return {
chosenExpenseId: null
};
},
computed: {
test() {
return this.expenseButton.reduce((acc, curr) => {
acc += curr.expensesValue;
return acc;
}, 0);
}
}
}
};
</script>
<template>
<div>
<div class="yourBalance">
Your monthly balance
<br />
<span>${{ test }}</span>
</div>
</div>
<template>
UPDATE
The "expenseValue" property within the "expenseButton" array is coming from a database on the backend using axios
Parent component
<template>
<div>
<expense-button :myExpense="myExpense" :expenseButton="expenseButton"></expense-button>
</div>
</template>
<script>
import Expenses from "../components/expenses.vue";
import axios from "axios";
export default {
components: {
"expense-button": Expenses
},
data() {
return {
budgetOwner: "",
myExpense: [],
expenseButton: [],
component: "",
errored: false
};
},
beforeRouteEnter(to, from, next) {
axios
.get("/api/budget", {
headers: { "Content-Type": "application/json" },
withCredentials: true
})
.then(res => {
next(vm => {
if (res.data.budget.length > 0) {
vm.myExpense = res.data.budget;
vm.expenseButton = res.data.budget[0].expenses;
}
});
})
.catch(err => {
next(vm => {
console.log(err.response);
vm.errored = true;
});
});
}
}
</script>
Data from database
"budget":[{"expenses":[
{"expensesKey":"a","expensesValue":1,"subExpenses":"","newValue":""},
{"expensesKey":"b","expensesValue":2,"subExpenses":"","newValue":""},
{"expensesKey":"c","expensesValue":3,"subExpenses":"","newValue":""}
]
Try this
test() {
if(this.expenseButton){
return this.expenseButton.reduce((acc, curr) => {
acc += curr.expensesValue;
return acc;
}, 0);
}
else{
return ''
}
}
Try to help you. The problem will be in curr.expensesValue. What is expensesValue? And one more question. Are you mount right your app? Are you have the same id in the root like a el:'#app' and div#id in my example?
new Vue({
el: "#app",
data:{
expenseButton:[1,2,3,4,5],
chosenExpenseId: null
},
computed: {
test() {
return this.expenseButton.reduce((acc, curr) => acc + curr)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="yourBalance">
Your monthly balance
<br />
<span>{{ test }}</span>
</div>
</div>