Error when attempting to retrieve one element using Vuex getter - vue.js

I'm creating a single page app using Vue/Vuex/Vue-router.
Basically I'm trying to retrieve one record after selecting it from a shown list, my store consists of basically:
export const store = new Vuex.Store({
state: {
reports: null,
loading: false,
reportProcessing: false
},
getters: {
getReports (state) {
return state.reports
},
getReport (state) {
return (id) => {
return state.reports.find((item) => {
return item.id === id
})
}
}
}
// ...
When I try to use it with
data () {
return {
// Attempt to load the report by passing the current id
report: JSON.parse(JSON.stringify(this.$store.getters.getReport(this.id))),
// ...
It shows an error for "SyntaxError: Unexpected token u in JSON at position 0" basically returns a null/empty object, which is really confusing because this works (selecting the first element from the object list) :
JSON.parse(JSON.stringify(this.$store.getters.getReports[0])),
So I know the object list contains the reports (and that the getters seem to run properly). It doesn't work however when attempting to pass the id manually like this.$store.getters.getReport(1)
Exactly what am I doing wrong here?
EDIT :
My current router file is set to (for the single report route)
{
path: '/report/:id',
props: true,
component: MainLayout,
children: [
{ path: '', name: 'edit_report', component: EditReport }
]
}
Basically I'm using vue-router's child routes to load the components inside a layout that has the main menu, however when I removed this function for that route to :
{
path: '/report/:id',
name: 'edit_report',
props: true,
component: EditReport
}
It worked (obviously without being loaded inside the main layout), needless to say this isn't a fix (since i still need it to load inside the main layout like all the other pages), but maybe it has some relation to what I'm doing wrong?

You are using an this.id that does not exist. The .find() in the getReports() getter will return undefined and the JSON.parse() will throw that error.
Here's a breakdown of JSON.parse(JSON.stringify(this.$store.getters.getReport(this.id))), with this.id equal to 6:
this.$store.getters.getReport(6) returns undefined
JSON.stringify(undefined) returns undefined
JSON.parse(undefined) throws Uncaught SyntaxError: Unexpected token u in JSON at position 0 error.
Demo below.
const store = new Vuex.Store({
strict: true,
state: {
reports: [{id: 1}, {id: 2}],
loading: false,
reportProcessing: false
},
getters: {
getReports (state) {
return state.reports
},
getReport (state) {
return (id) => {
return state.reports.find((item) => {
return item.id === id
})
}
}
}
});
new Vue({
store,
el: '#app',
computed: {
reports: function() {
return this.$store.state.reports
},
},
methods: {
callGetReport() {
console.log(this.$store.getters.getReport(6));
console.log(JSON.stringify(this.$store.getters.getReport(6)));
console.log(JSON.parse(JSON.stringify(this.$store.getters.getReport(6))));
}
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
<p>Reports: {{ reports }}</p>
<button #click="callGetReport">Click here to call getReport() - open browser's console to see result</button>
</div>
Passing props to child (nested) routes
You are not getting the id in the nested route because the props are not turned on:
{
path: '/report/:id',
props: true,
component: MainLayout,
children: [
{ path: '', name: 'edit_report', component: EditReport, props: true }
// ^^^^^^^^^^^^^ ------ added this
]
}

Related

Vuex mapState based on route and route parameters

I have a works component I use different pages on my app and I am trying to load the state based on the route and route parameters.
In my App.vue file I dispatch the async action to get the json file like
mounted() {
this.$store.dispatch('getData')
},
And I map the state in my works component like that
export default {
name: 'Works',
computed: mapState({
works: (state) => state.works.home.items.slice(0, state.works.home.loadedCount),
loadedCount: (state) => state.works.home.loadedCount,
totalCount: (state) => state.works.home.items.length,
})
}
I actually need to map the state dynamically based on the route just like state.works[this.$router.currentRoute.params.category] or based on route name.
Could you please tell me what is the correct way to get the data (async) from my state?
Vuex store:
export default new Vuex.Store({
state: {
works: {
all: {
items: [],
loadedCount: 0,
},
home: {
items: [],
loadedCount: 0,
},
web: {
items: [],
loadedCount: 0,
},
print: {
items: [],
loadedCount: 0,
},
},
limit: 2,
},
mutations: {
SET_WORKS(state, works) {
state.works.all.items = works
works.map((el) => {
if (typeof state.works[el.category] !== 'undefined') {
state.works[el.category].items.push(el)
}
})
},
},
actions: {
getData({ commit }) {
axios
.get('/works.json')
.then((response) => {
commit('SET_WORKS', response.data.works)
})
},
},
})
You can do it in beforeCreate hook.
beforeCreate(){
const category = this.$route.params.category;
Object.assign(this.$options.computed, {
...mapState({
categoryItems: (state) => state.categories[category],
}),
});
}
I've created a basic working example: https://codepen.io/bgtor/pen/OJbOxKo?editors=1111
UPDATE:
To get mapped properties updated with route change, you will have to force re-render the component. The best way to do it, is to change the component key when route change in parent component.
Parent.vue
<template>
<categoryComponent :key="key"></categoryComponent> // <-- This is the component you work with
</template>
computed: {
key(){
return this.$route.params.category
}
}
With this approach the beforeCreate hook will be triggered with every route change, getting fresh data from Vuex.

Vue.js, Vuex; component view not reacting when data in the Vuex store is mutated (with mysterious exception)

This is my first foray into using Vuex, and I'm baffled by a problem relating to a searchResults array, managed in the $store, specifically why a SearchResults view component doesn't seem to be reacting when the store is mutated.
I have a search form component which, when submitted, invokes a search function (mixin), dispatches an action which updates the searchResults array in the store, and then loads ViewSearchResults.vue, where the search results are displayed - and this is working.
Now, ViewSearchResults.vue also includes the search form component, and when a subsequent search is run, again the search function runs successfully, the store is updated accordingly, however ViewSearchResults.vue is not reacting to the change in the store, e.g., update lifecycle doesn't fire, so the new search results are unavailable
... and then in my debugging journey I discovered that by adding a reference to the store in the template - e.g., {{ this.$store.state.searchResults.length }}, the view updates, the new data is available, and any subsequent searches successfully update the view.
None of my experience with Vue.js so far explains this. Can someone please shed some light on this, and how I can realize the desired results without polluting my markup?.
Many thanks in advance.
relevant excerpt of my search mixin:
export default {
created: function() {},
methods: {
doSearch: function(term) {
const searchTerm = term.toLowerCase();
this.$store.dispatch("setSearchTerm", term);
let searchResults = [];
// SNIP: search (iterate) a bunch of .json data ...
searchResults.push(searchResult); // searchResults array CONFIRMED √
this.$store.dispatch("setSearchResults", searchResults);
}
}
}
relevant excerpt of the store:
export default new Vuex.Store({
strict: true,
state: {
searchTerm: "",
searchResults: [],
},
mutations: {
setSearchTerm(state, payload) {
state.searchTerm = payload;
},
setSearchResults(state, payload) {
console.log(payload); // √ confirmed: updated array is there
state.searchResults = payload;
console.log(state.searchResults); // √ confirmed: updated array is there
}
},
getters: {
},
actions: {
// dispatched in the search mixin
setSearchTerm(context, payload){
context.commit("setSearchTerm", payload);
},
setSearchResults(context, payload) {
context.commit("setSearchResults", payload);
}
},
modules: {
}
})
... and ViewSearchResults.vue (relevant excerpts):
// IF I LEAVE THIS IN, BOB'S YOUR UNCLE ... WITHOUT IT, THE VIEW DOESN'T REACT
<div style="display: none;">this.$store.state.searchResults: {{ this.$store.state.searchResults.length }}</div>
<ul class="search-results">
<li v-for="(imgObj, ix) in searchResults" :key="ix">
<img :src="require('#/assets/img/collections/' + imgObj.path + '/' + imgObj.data + '/' + imgObj.imgFile)" alt="" />
</li>
</ul>
export default {
components: {
// 'app-search' occurs elswhere in the app, but when submitted, loads this ViewSearchResults, search component still present
'app-search': SearchForm
},
props: {
},
data() {
return {
searchTerm: "",
searchResults: []
}
},
created: function() {
// only becuz refresh
if (!this.searchTerm) {
this.searchTerm = this.$route.params.searchTerm;
}
console.log(this.$store.state.searchResults.length); // 0 if refreshed, ERGO:
this.$store.state.searchResults.length ? this.searchResults = this.$store.state.searchResults : this.searchResults = JSON.parse(localStorage.getItem("searchResults"));
console.log(this.searchResults); // searchResults √
},
updated: function() {
// ?!?!?! WHY DOES THIS FIRE ONLY IF I LEAVE THE REFERENCE TO THE STORE IN THE TEMPLATE? {{ this.$store.state.searchResults.length }}
this.$store.state.searchTerm ? this.searchTerm = this.$store.state.searchTerm : this.searchTerm = localStorage.getItem("searchTerm");
this.$store.state.searchResults.length ? this.searchResults = this.$store.state.searchResults : this.searchResults = JSON.parse(localStorage.getItem("searchResults"));
},
computed: {
},
mounted: function() {},
mixins: [ Search ]
}
Many thanks again for any insight.
Whiskey T.
You've got nothing updating in your component so it won't need to execute the update hook.
It seems you actually want your component to be driven by the values in the store.
I would set it up as recommended in the Vuex guide
computed: {
searchResults () {
return this.$store.state.searchResults
}
},
created () {
this.doSearch(this.$route.params.searchTerm)
}
You could also use the mapState helper if you wanted.
computed: mapState(['searchResults']),
The part where you load data from localstorage should be done in your store's state initialiser, ie
let initialSearchResults
try {
initialSearchResults = JSON.parse(localStorage.getItem('searchResults'))
} catch (e) {
console.warn('Could not parse saved search results')
initialSearchResults = []
}
export default new Vuex.Store({
strict: true,
state: {
searchTerm: "",
searchResults: initialSearchResults
},

Displaying data from Vuex store in component based on route

I have the following data in Vuex store:
state: {
news: [
{ id: 1, title: "placeholder", text: "Lorem ipsum doloret imes", date: "20-01-2020", type: "management" },
{ id: 2, title: "Revenue", text: "Lorem ipsum doloret imes", date: "20-01-2020", type: "management" }]
}
I want to display this data in the component based on the id used in route:
{path: '/news/:id',
component: () => import('../views/NewsDetail.vue'),
props: true
}
In my NewsDetail.vue component I try to retrieve the data like this:
<template>
<p class="display-1 text--primary">{{display.type}}</p>
</template>
<script>
data () {
return {
display: newsId
}
},
created () {
const newsId = this.$store.state.news.find((newsId) => { return newsId.id == this.$route.params.id})
}
</script>
But I get error that newsId is not defined and that it is defined but never used...
How can I display the data from the vuex store based on route id (that should be matching the id of the entry in store)?
'error that newsId is not defined'
so what you want is vuex getters
<script>
import { mapGetters } from 'vuex'
computed: {
...mapGetters([
news
]),
newsId() {
return this.news.find((newsId) => { return newsId.id == this.$route.params.id})
}
}
it's either that or adding newsId to the data object
<script>
data () {
return {
newsId: '',
display: newsId
}
},
created () {
this.newsId = this.$store.state.news.find((newsId) => { return newsId.id ==
this.$route.params.id})
}
</script>
Your newsId constant is defined in the created() method and only exists within that scope. It is deleted as soon as the function returns. That's why you're getting the error message--because the const isn't used inside the function, and it's not available to be used anywhere else.
I suggest you create an id prop in your NewsDetail component, which will automatically be populated with the ID param. Then use a computed property to fetch the appropriate data from the store.

how to get nested getters in vuex nuxt

i have store/index.js like this
new Vuex.Store({
modules: {
nav: {
namespaced: true,
modules: {
message: {
namespaced: true,
state: {
count: 0,
conversations: [],
},
getters: {
getCount: state => {
return state.count;
},
},
mutations: {
updateCount(state) {
state.count++;
},
},
actions: {},
},
requests: {
namespaced: true,
state: {
friends: [],
},
getters: {
getFriends: state => {
return state.friends;
},
},
mutations: {
pushFriends(state, data) {
state.friends.push(data);
},
},
actions: {
pushFriends(commit, data) {
commit('pushFriends', data);
},
},
},
},
},
},
});
i want to use getters in computed property i have tested like this
computed: {
...mapGetters({
count: 'nav/message/getCount',
}),
},
butt getting error
[vuex] unknown getter: nav/message/getCount
what is am missing here
i also want to make separate folder for every modules like my nav have 3 modules message, requests & notifications
i did try but nuxt blow up my codes
I think your index is wrong, the correct thing is to separate the modules independently, something like this:
in your store/index.js
export const state = () => ({
config: {
apiURL: 'https://meuapp.com'
}
})
export const mutations = { }
export const actions = { }
// getters
export const getters = {
test: state => payload => {
if (!payload)
return {
message: 'this is an messagem from index without payload test.', // you don't need pass any payload is only to show you how to do.
result: state.config
}
else
// return value
return {
message: 'this is an message from index test with payload.',
result: state.config, // here is your index state config value
payload: payload // here is yours params that you need to manipulate inside getter
}
}
}
here is your store/navi.js
export const state = () => ({
navi: {
options: ['aaa', 'bbb', 'ccc']
}
})
export const mutations = { }
export const actions = { }
// getters
export const getters = {
test: state => payload => {
if (!payload)
return {
message: 'this is a messagem from nav store without payload test.', // you don't need pass any payload is only to show you how to do.
result: state.navi
}
else
// return value
return {
message: 'this is an messagem from navi test with payload.',
result: state.navi, // here is your index state config value
payload: payload // here is yours params that you need to manipulate inside getter
}
}
}
then in your component you can use as a computed properties:
<template>
<div>
without a paylod from index<br>
<pre v-text="indexTest()" />
with a paylod from index<br>
<pre v-text="indexTest( {name: 'name', other: 'other'})" />
without a paylod from navi<br>
<pre v-text="naviTest()" />
with a paylod from navi<br>
<pre v-text="naviTest( {name: 'name', other: 'other'})" />
access getters from methods<br>
<pre>{{ accessGetters('index') }}</pre>
<pre v-text="accessGetters('navi')" />
<br><br>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
computed: {
...mapGetters({
indexTest: 'test',
naviTest: 'navi/test'
})
},
methods: {
accessGetters (test) {
if (test && test === 'index' ) {
console.log('test is', test) // eslint-disable-line no-console
return this.indexTest()
}
else if (test && test === 'navi') {
console.log('test is:', test) // eslint-disable-line no-console
return this.naviTest()
}
else {
return 'test is false'
}
}
}
}
</script>
Whenever possible separate your code into smaller parts, one part for each thing. This makes it easier for you to update and keep everything in order.
Hope this helps.
I came here to find a way to access the getters of a module that was nested inside another module in Vue.js and the following solution worked for me:
this.$store.getters['outerModuleName/innerModuleName/nameOfTheGetter']
Maybe this helps someone with a similar problem.

Use v-model with groupBy array, return flat array

I'm trying to set up a Vue component that takes a flat list of items in an array, groups them by a property for use in a sub-component, and emits the updated flat array.
My section component uses these grouped items in their v-model and emits the updated list. The section component is a drag-and-drop with some input fields, so items are changed under the section component and the updated list is emitted.
Here's an example of the component that takes the flat list as a prop:
<template>
<div>
<div v-for="section in template.sections" :key="section.id">
<h2>{{ section.name }}</h2>
<item-section :section="section" v-model="sectionData[section.id]"></item-section>
</div>
</div>
</template>
<script type="text/javascript">
import { groupBy } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
name: "ItemAssignment",
props: {
// All items in flat array
value: {
type: Array,
required: true,
default: () => [
/**
* {
* id: null,
* section_id: null,
* name: null
* }
*/
]
},
// Template (containing available sections)
template: {
type: Object,
default: () => {
return {
sections: [
/**
* {
* id: null,
* name: null
* }
*/
]
};
}
}
},
components: {
ItemSection
},
data() {
return {
sectionData: []
};
},
mounted() {},
computed: {
flattenedData() {
return Object.values(this.sectionData).flat();
}
},
methods: {},
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val) {
this.sectionData = groupBy(val, "section_id");
}
},
// --- Causing infinite loop ---
// flattenedData(val) {
// this.$emit("input", val);
// },
}
};
</script>
The parent of this component is basically this:
<template>
<div>
<!-- List items should be updatable here or from within the assignment component -->
<item-assignment v-model="listItems"></item-assignment>
</div>
</template>
<script type="text/javascript">
import ItemAssignment from "#/components/ItemAssignment.vue";
export default {
name: "ItemExample",
props: {
},
components: {
ItemAssignment
},
data() {
return {
listItems: []
};
},
mounted() {},
computed: {
},
methods: {
// Coming from API...
importExisting(list) {
var newList = [];
list.forEach(item => {
const newItem = {
id: null, // New record, so don't inherit ID
section_id: item.section_id,
name: item.name
};
newList.push(newItem);
});
this.listItems = newList;
}
},
watch: {
}
};
</script>
When emitting the finalized flat array, Vue goes into an infinite loop trying to re-process the list and the browser tab freezes up.
I believe the groupBy and/or Object.values(array).flat() method are stripping the reactivity out so Vue constantly thinks it's different data, thus the infinite loop.
I've tried manually looping through the items and pushing them to a temporary array, but have had the same issue.
If anyone knows a way to group and flatten these items while maintaining reactivity, I'd greatly appreciate it. Thanks!
So it makes sense why this is happening...
The groupBy function creates a new array, and since you're watching the array, the input event is triggered which causes the parent to update and pass the same value which gets triggered again in a loop.
Since you're already using lodash, you may be able to include the isEqual function that can compare the arrays
import { groupBy, isEqual } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
// ...redacted code...
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val, oldVal) {
if (!isEqual(val, oldVal))
this.sectionData = groupBy(val, "section_id");
}
},
flattenedData(val) {
this.$emit("input", val);
},
}
};
this should prevent the this.sectionData from updating if the old and new values are the same.
this could also be done in flattenedData, but would require another value to store the previous state.