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,
};
});
},
Related
I created a todo app, and now I'm trying to delete the todo data one by one, but I'm getting the error stating that TypeError: Cannot read properties of undefined (reading 'id'). Don't know what to do next. I'm using the vuex concept and just help me to clear the error.
App.vue:
<template>
<div>
<input type="text" v-model="title">
<button #click="onSubmit">submit</button>
<div v-for="datas in allData" :key="datas.id">
<h1>{{datas.title}}</h1>
<i class="fas fa-trash-alt" #:click="deleteData(todo.id)"></i>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
data() {
return {
title: ''
}
},
methods: {
...mapActions(['fetchData', 'newData', 'deleteData']),
onSubmit(e) {
e.preventDefault();
this.newData(this.title)
},
},
computed: mapGetters(['allData']),
created() {
this.fetchData();
}
}
</script>
index.js (store);
import axios from 'axios'
import { createStore } from 'vuex'
export default createStore({
state: {
todos: [],
},
getters: {
allData: (state) => {
return state.todos
}
},
mutations: {
setData: (state, todoData) => (state.todos = todoData),
new: (state, todo) => (state.todos.unshift(todo)),
removeData: (state, id) => state.todos = state.todos.filter((todo) => todo.id !== id),
},
actions: {
async fetchData({ commit }) {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos')
commit('setData', response.data)
},
async newData({ commit }, title) {
const response = await axios.post('https://jsonplaceholder.typicode.com/todos', { title, completed: false })
commit('new', response.data)
},
async deleteData({ commit }, id) {
await axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`);
commit('removeData', id)
}
}
})
In the template, you iterate through array and passing todo.id to delete function, why not extracted item from v-for loop dates.id?
<template>
<div>
<input type="text" v-model="title">
<button #click="onSubmit">submit</button>
<div v-for="item of data" :key="item.id">
<h1>{{item.title}}</h1>
<i class="fas fa-trash-alt" #:click="deleteData(item.id)"></i>
</div>
</div>
</template>
And remove s in your data name because you extract one item, not items from loop.
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>
I would like to create dynamic sidebar links pulled from an API.
This is the theme I'm working with:
https://admin.vuebulma.com/#/
Note the charts link that has a list of children in the sidebar..
I want to make an API request - lets say for charts and create each child (see charts.js) for each element return in the API request.
In the example below, the data objects are hardcoded - I would like to remove this and dynamically create each child using a for loop for each element in the api request.body
/store/modules/menu/index.js
import * as types from '../../mutation-types'
import lazyLoading from './lazyLoading'
import charts from './charts'
import components from './components'
import dashboard from './dashboard'
// show: meta.label -> name
// name: component name
// meta.label: display label
const state = {
items: [
dashboard,
charts,
components
]
}
const mutations = {
[types.EXPAND_MENU] (state, menuItem) {
if (menuItem.index > -1) {
if (state.items[menuItem.index] && state.items[menuItem.index].meta) {
state.items[menuItem.index].meta.expanded = menuItem.expanded
}
} else if (menuItem.item && 'expanded' in menuItem.item.meta) {
menuItem.item.meta.expanded = menuItem.expanded
}
}
}
export default {
state,
mutations
}
/store/modules/menu/charts.js
import lazyLoading from './lazyLoading'
export default {
name: 'Charts',
path: '/charts',
meta: {
icon: 'fa-bar-chart-o',
expanded: false,
link: 'charts/index.vue'
},
component: lazyLoading('charts', true),
children: [
{
name: 'Chartist',
path: 'chartist',
component: lazyLoading('charts/Chartist'),
meta: {
link: 'charts/Chartist.vue'
}
},
{
name: 'Chartjs',
path: 'chartjs',
component: lazyLoading('charts/Chartjs'),
meta: {
link: 'charts/Chartjs.vue'
}
},
{
name: 'Peity',
path: 'peity',
component: lazyLoading('charts/Peity'),
meta: {
link: 'charts/Peity.vue'
}
},
{
name: 'Plotly',
path: 'plotly',
component: lazyLoading('charts/Plotly'),
meta: {
link: 'charts/Plotly.vue'
}
}
]
}
sidebar.vue
<template>
<aside class="menu app-sidebar animated" :class="{ slideInLeft: show, slideOutLeft: !show }">
<p class="menu-label">
General
</p>
<ul class="menu-list">
<li v-for="(item, index) in menu">
<router-link :to="item.path" :exact="true" :aria-expanded="isExpanded(item) ? 'true' : 'false'" v-if="item.path" #click.native="toggle(index, item)">
<span class="icon is-small"><i :class="['fa', item.meta.icon]"></i></span>
{{ item.meta.label || item.name }}
<span class="icon is-small is-angle" v-if="item.children && item.children.length">
<i class="fa fa-angle-down"></i>
</span>
</router-link>
<a :aria-expanded="isExpanded(item)" v-else #click="toggle(index, item)">
<span class="icon is-small"><i :class="['fa', item.meta.icon]"></i></span>
{{ item.meta.label || item.name }}
<span class="icon is-small is-angle" v-if="item.children && item.children.length">
<i class="fa fa-angle-down"></i>
</span>
</a>
<expanding v-if="item.children && item.children.length">
<ul v-show="isExpanded(item)">
<li v-for="subItem in item.children" v-if="subItem.path">
<router-link :to="generatePath(item, subItem)">
{{ subItem.meta && subItem.meta.label || subItem.name }}
</router-link>
</li>
</ul>
</expanding>
</li>
</ul>
</aside>
</template>
<script>
import Expanding from 'vue-bulma-expanding'
import { mapGetters, mapActions } from 'vuex'
export default {
components: {
Expanding
},
props: {
show: Boolean
},
data () {
return {
isReady: false
}
},
mounted () {
let route = this.$route
if (route.name) {
this.isReady = true
this.shouldExpandMatchItem(route)
}
},
computed: mapGetters({
menu: 'menuitems'
}),
methods: {
...mapActions([
'expandMenu'
]),
isExpanded (item) {
return item.meta.expanded
},
toggle (index, item) {
this.expandMenu({
index: index,
expanded: !item.meta.expanded
})
},
shouldExpandMatchItem (route) {
let matched = route.matched
let lastMatched = matched[matched.length - 1]
let parent = lastMatched.parent || lastMatched
const isParent = parent === lastMatched
if (isParent) {
const p = this.findParentFromMenu(route)
if (p) {
parent = p
}
}
if ('expanded' in parent.meta && !isParent) {
this.expandMenu({
item: parent,
expanded: true
})
}
},
generatePath (item, subItem) {
return `${item.component ? item.path + '/' : ''}${subItem.path}`
},
findParentFromMenu (route) {
const menu = this.menu
for (let i = 0, l = menu.length; i < l; i++) {
const item = menu[i]
const k = item.children && item.children.length
if (k) {
for (let j = 0; j < k; j++) {
if (item.children[j].name === route.name) {
return item
}
}
}
}
}
},
watch: {
$route (route) {
this.isReady = true
this.shouldExpandMatchItem(route)
}
}
}
</script>
Not sure why I can't figure out how to do this.
Update:
Here's an example of the API I'm calling:
[
{
"id": 1,
"name": "test1",
"os": "windows",
"url": "https://test.com"
},
{
"id": 2,
"name": "test2",
"os": "ios",
"url": "https://test.com"
},
{
"id": 1,
"name": "test3",
"os": "windows",
"url": "https://test.com"
},
]
Create a copy of routes of charts in store
const state = {
chartsRoutes: []
}
Create a computed property in component
computed: {
chartsRoutes () {
return this.$store.state.chartsRoutes
}
}
Use v-for to render chartsRoutes into router-links in component
Create a mutation to modify store and router
// import router
const mutations = {
'update-charts-routes': function (state, payload) {
const { chartsRoutes } = payload
state.chartsRoutes = chartsRoutes.map(r => {
return {
path: '/your/custom/path/according/to/response'
// other params
}
})
router.addRoutes(state.chartsRoutes)
}
}
Create an action
const actions = {
'reload-charts': function ({commit, dispatch}, data) {
return new Promise((resolve, reject) => {
const r = {
method: 'get',
url: data.url,
// add more options, e.g. header or auth
}
axios.request(r)
.then(resp => {
commit('update-charts-routes', { chartsRoutes: resp.data })
resolve()
})
.catch(err => {
// handle error
reject(err)
})
}
}
}
}
Dispatch action
this.$store.dispatch('reload-charts', { url: 'http://some.host/path/to/url' })
.then(() => {
// other stuff
})
Why is my prop returning undefined? I'm trying to access 'total', but its undefined. I pass it in as a prop but it logs to the console as undefined and its also returning blank on the page. I think I might be making a mistake somewhere, because I have done it this way before and it worked.
Here is my container Store.js
import { connect } from 'react-redux';
import { getItems, getCurrency, getTotal} from '../ducks/cart';
import Store from '../components/Store';
const mapStateToProps = (state, props) => {
return {
total: getTotal(state, props)
}
}
export default connect(mapStateToProps)(Store);
Here is my Store.js component:
import React, {Component} from 'react';
import { PropTypes } from 'react';
import Home from '../components/Home';
import Cart from '../containers/Cart';
import ProductList from '../containers/ProductList';
import Checkout from '../containers/Checkout';
const Store = ({ total }) => {
console.error(total);
return(
<div className="container">
<div className="row">
<div className="col-md-12">
<h3>Armor and Weapon Store</h3>
<h4 className="badge badge-warning margin-right">Gold: {total} </h4>
</div>
</div>
<div className="row">
<div className="col-md-8">
<ProductList />
</div>
<div className="col-md-4">
<Cart />
<Checkout />
</div>
</div>
</div>
);
}
Store.propTypes = {
total: PropTypes.number,
}
export default Store;
And here is my redux stuff Cart.js:
import { getProduct } from '../ducks/products';
// actions
const CART_ADD = 'cart/ADD';
const CART_REMOVE = 'cart/REMOVE';
// reducer
const initialState = {
items: [], // array of product ids
currency: 'GOLD'
};
export default function cart(state = initialState, action = {}) {
switch (action.type) {
case CART_ADD:
return handleCartAdd(state, action.payload);
case CART_REMOVE:
return handleCartRemove(state, action.payload);
default:
return state;
}
}
function handleCartAdd(state, payload) {
return {
...state,
items: [ ...state.items, payload.productId ]
};
}
function handleCartRemove(state, payload) {
return {
...state,
items: state.items.filter(id => id !== payload.productId)
};
}
// action creators
export function addToCart(productId) {
return {
type: CART_ADD,
payload: {
productId
}
}
}
export function removeFromCart(productId) {
return {
type: CART_REMOVE,
payload: {
productId
}
}
}
// selectors
export function isInCart(state, props) {
return state.cart.items.indexOf(props.id) !== -1;
}
export function getItems(state, props) {
return state.cart.items.map(id => getProduct(state, { id }));
}
export function getCurrency(state, props) {
return state.cart.currency;
}
export function getTotal(state, props) {
return state.cart.items.reduce((acc, id) => {
const item = getProduct(state, { id });
const total = acc + item.price;
return acc + item.price;
}, 0);
}