The error appears when I use a v-for loop to go through the 'allPosts' data on my div.
The Nuxt documentation says 'Modules: every .js file inside the store directory is transformed as a namespaced module'. Maybe I'm missing something in this regard?
pages/index.vue
<template>
<section id="postgrid">
<div v-for="post in allPosts" :key="post.id"></div>
</section>
</template>
<script>
import {mapGetters} from 'vuex'
import PostTile from '#/components/Blog/PostTile'
export default {
components: {
PostTile
},
computed: mapGetters(['allPosts'])
}
</script>
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Posts from './posts'
const store = new Vuex.Store({
modules: {
Posts
}
})
store/posts.js
const state = () => ({
posts: [
{
id: 0,
title: 'A new beginning',
previewText: 'This will be awesome don\'t miss it',
category: 'Food',
featured_image: 'http://getwallpapers.com/wallpaper/full/6/9/8/668959.jpg',
slug: 'a-new-beginning',
post_body: '<p>Post body here</p>',
next_post_slug: 'a-second-beginning'
},
{
id: 1,
title: 'A second beginning',
previewText: 'This will be awesome don\'t miss it',
category: 'Venues',
featured_image: 'https://images.wallpaperscraft.com/image/beautiful_scenery_mountains_lake_nature_93318_1920x1080.jpg',
slug: 'a-second-beginning',
post_body: '<p>Post body here</p>',
prev_post_slug: 'a-new-beginning',
next_post_slug: 'a-third-beginning'
},
{
id: 2,
title: 'A third beginning',
previewText: 'This will be awesome don\'t miss it',
category: 'Experiences',
featured_image: 'http://eskipaper.com/images/beautiful-reflective-wallpaper-1.jpg',
slug: 'a-third-beginning',
post_body: '<p>Post body here</p>',
prev_post_slug: 'a-second-beginning',
next_post_slug: 'a-forth-beginning'
}
]
})
const getters = {
allPosts: (state) => state.posts
}
export default {
state,
getters
}
You have a number of issues in how you are setting up and accessing your store. Firstly you are creating your store using the "classic mode" which the docs tell us:
This feature is deprecated and will be removed in Nuxt 3.
So in order to be using the latest methods your store/index.js should look like this:
//store/index.js
//end
This is not a mistake, you don't actually need anything in it, just have it exist. There is no need to import vue or vuex or any modules.
Your store/posts.js can largely stay as it is, just change your state, mutations, getters, and actions to be exported constants and delete the bottom export:
//store/posts.js
export const state = () => ({
posts: [
...
]
})
export const mutations = {
}
export const actions = {
}
export const getters = {
allPosts: state => state.posts
}
//delete the following
export default {
state,
getters
}
Secondly you seem to be using mapGetters incorrectly. If you set up your store like I have above, you can use it in pages/index.vue like so:
//pages.index.vue
<script>
import {mapGetters} from 'vuex'
export default {
computed: {
...mapGetters ({
allposts: 'posts/allPosts'
})
}
}
</script>
Then you can access "allPosts" in your template as you would any computed property or access it with "this.allPosts" in your script.
Related
I'm building a web app using Nuxt v2.15 and #nuxtjs/i18n v7.2. I'm using Vuex for state management. In my global state, I want to create a getter that returns a value based on this.$i18n.locale.
What is the best way to access the current app context, which has $i18n attached to it, in my Vuex getter method?
nuxt.config.js
export default {
modules: [
"#nuxtjs/i18n", // https://i18n.nuxtjs.org/
],
i18n: {
locales: { /* configuration */ },
defaultLocale: "en",
detectBrowserLanguage: {
fallbackLocale: "en",
redirectOn: "root",
},
langDir: "~/locales/",
vueI18n: {
fallbackLocale: "en",
messages: { /* translation data */ },
},
},
};
store/index.js
export const state = () => ({
// Root module for Vuex store: https://nuxtjs.org/docs/directory-structure/store/
});
export const getters = {
loginOpts() {
const locale = this.$i18n.locale;
const uiLocales = [locale];
if (locale === "zh") {
uiLocales.push("zh-CN");
}
return { params: { ui_locales: uiLocales } };
},
};
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', $store.getters.loginOpts)">Sign In</v-btn>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Login",
data() {
return {};
},
computed: {
...mapGetters(["loginOpts"]),
},
};
</script>
What I expect
I expect to be able to access this.$i18n from a Vuex getter, or to have some means of doing so.
What actually happens
In the getter method, this is undefined.
TypeError: Cannot read properties of undefined (reading '$i18n')
What I've tried
I see here that a getter is passed four arguments: state, getters, rootState, and rootGetters.
I've tried:
RTFM for Vuex State, Getters, and Nuxt i18n
accessing state.$i18n
adding $i18n: this.$i18n to the root state
I had a similar problem last week where I absolutely needed to use i18n inside a vuex getter.
I wouldn't recommend this as its probably not the most performant thing in the world but it worked for me and solved a huge problem while I was working on a government application.
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', theLoginOpts)">Sign In</v-btn>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Login",
data() {
return {
theLoginOpts: $store.getters.loginOpts(this) // The secret sauce
};
},
computed: {
...mapGetters(["loginOpts"]),
},
};
</script>
store/index.js
export const state = () => ({
// Root module for Vuex store: https://nuxtjs.org/docs/directory-structure/store/
});
export const getters = {
loginOpts: (state) => (app) => { // App is the secret sauce from earlier
const locale = app.$i18n.locale;
const uiLocales = [locale];
if (locale === "zh") {
uiLocales.push("zh-CN");
}
return { params: { ui_locales: uiLocales } };
},
};
After digging through docs, experimenting, and discussing with the folks who were kind enough to offer answers here, it is clear that i18n is not directly accessible in a Vuex getter method, despite the fact that #nuxt/i18n registers a Vuex module called i18n and everything I've read about Vuex modules suggests this should be possible.
I did however come across the docs for #nuxt/i18n callbacks, which led me to create a small plugin that sets the value of locale and uiLocales in the global state using a mutation.
The end result looks like this:
nuxt.config.js
export default {
modules: [
"#nuxtjs/i18n", // https://i18n.nuxtjs.org/
],
plugins: [
"~/plugins/i18n.js",
],
i18n: {
locales: { /* configuration */ },
defaultLocale: "en",
detectBrowserLanguage: {
fallbackLocale: "en",
redirectOn: "root",
},
langDir: "~/locales/",
vueI18n: {
fallbackLocale: "en",
messages: { /* translation data */ },
},
},
};
plugins/i18n.js
export function findLocaleConfig (i18n, locale) {
return (
i18n.locales.find(({ iso, code }) => [iso, code].includes(locale)) || {}
);
}
export default function ({ app }) {
app.store.commit("localeChange", findLocaleConfig(app.i18n, app.i18n.locale));
app.i18n.onLanguageSwitched = (oldLocale, newLocale) => {
app.store.commit("localeChange", findLocaleConfig(app.i18n, newLocale));
};
}
store/index.js
export const state = () => ({
locale: null,
uiLocales: [],
});
export const mutations = {
localeChange(state, locale) {
state.locale = locale.code;
state.uiLocales = [locale.code, locale.iso];
},
};
components/login.vue
<template>
<div>
<v-btn v-if="$auth.loggedIn" #click="$auth.logout()">Sign Out</v-btn>
<v-btn v-else #click="$auth.loginWith('auth0', loginOpts)">Sign In</v-btn>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {};
},
computed: {
loginOpts() {
const uiLocales = this.$store.state.uiLocales;
return { params: { ui_locales: uiLocales } };
},
},
};
</script>
You can access a the locale in an Vuex action with: this.app.i18n.locale.
This cannot be accessed in a state or getter (a getter is a not a place for it anyway IMO).
PS: The above means that means that you can access this value anywhere you have access to the Nuxt context.
The simple way we did in our project is like below:
In component computed prop
teamsForSelector() {
return this.$store.getters['company/teamsForSelector'](this.$i18n);
}
and in state getters
teamsForSelector: (state) => ($i18n) => {
const teamsForSelector = state.teams.map((team) => {
return {
id: team.teamUid,
label: team.teamName,
};
});
teamsForSelector.unshift({
id: 'all-teams',
label: $i18n.t('key'),
});
return teamsForSelector;
}
Here is my post.js in store:
import axios from 'axios'
import createPersistedState from "vuex-persistedstate"
export default {
namespaced: true,
state: {
sample_data: 'Welcome!!',
logged_in: false,
},
getters: {
getSampleData: state => state.sample_data
},
mutations: {
},
actions: {
},
}
And in my component:
<template>
<div class="home">
<p>{{ sample_data }}</p>
<p>{{ logged_in }}</p>
</div>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
export default {
name: 'Home',
components: {
},
data() {
return {
msg: 'Welcome'
}
},
methods: {
},
computed: {
...mapState('post', ['sample_data', 'logged_in'])
}
}
</script>
I have installed the Vue extension in Chrome.
Now when I change the sample_data in DevTools > Vue > Vuex from "welcome" to "welcome 2222", it will exactly say "welcome 2222" on the page. Now when I hit refresh, the page goes back in saying "welcome" but in the Vuex panel it says the sample_data: welcome 2222. I tried refreshing the Vue panel but it will still say sample_data: welcome 2222 so the persisted state must be working. Why is my page though displays the old "welcome" and not the "welcome 2222"?
Thanks!
UPDATE
I tried putting this:
mutations: {
setSampleData(state, payload) {
state.sample_data = payload
}
},
actions: {
setSampleData({commit}, payload) {
commit('setSampleData', payload)
}
},
And in my component:
<button #click="setSampleData('Hello Hero')">Get Data</button>
Now it changed the state to "Hello Hero" even if I refresh the page. It seems to be working. However again, in the Vuex panel, it will still say something like "welcome 2222".
If I change the data in the Vuex panel to "hi there", the page will say "hi there". But when I refresh, it will display "Hello Hero" again and in the Vuex panel it says "hi there". Seems there's a problem between the two?
UPDATE 2
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import post from './post'
import createPersistedState from "vuex-persistedstate"
import * as Cookies from 'js-cookie'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
post
},
plugins: [createPersistedState()],
})
It seems you didn't meet this requirement from the package documentation:
New plugin instances can be created in separate files, but must be imported and added to plugins object in the main Vuex file
import React from 'react'
import sinon from 'sinon'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import FileListEditable from './index.js'
import {shallow} from 'enzyme'
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {
customer: {
clientId:'123'
}
}
const store = mockStore(initialState)
const minProps = {
files: []
}
const removeFile = sinon.spy()
const wrapper = shallow(
<FileListEditable
store={store}
{...minProps}
removeFile={removeFile} />,
{context: {store}})
test.skip('Component: <FileListEditable/>, renders', () => {
expect(wrapper.length).toBe(1)
expect(wrapper.find('Tag').length).toBe(0)
})
test.skip('Component <FileListEditable/>, Add and remove files', () => {
wrapper.setProps({
files: [
{
name: 'file1',
extension: 'txt'
},
{
name: 'file2',
extension: 'txt'
}
]
})
expect(wrapper.find('Tag').length).toBe(2)
wrapper.find('Tag').at(0).find('button').simulate('click')
expect(removeFile.called).toBe(true)
expect(removeFile.args[0][0]).toBe(0)
wrapper.find('Tag').at(1).find('button').simulate('click')
expect(removeFile.args[1][0]).toBe(1)
})
test.skip('Component <FileListEditable/>, File from documents will have link to that document', () => {
wrapper.setProps({
files: [
{
name: 'file1',
extension: 'txt',
id: 'file-document-id'
},
{
name: 'file2',
extension: 'txt'
}
]
})
expect(wrapper.find('Tag').at(0).find('a').length).toBe(1)
expect(wrapper.find('Tag').at(1).find('a').length).toBe(0)
})
These tests do not work because FileListEditable is wrapped with injectIntl and one of our own created higher order component. Which means when I use shallow rendering it will render the InjectIntl component and if I use mount I have to dive two layers. But I just can't seem to get it right. Is there a general solution for testing components that are wrapped with higher order components without having to care about the higher order component?
Thank you Daniel Lizik for sharing the link
https://github.com/airbnb/enzyme/issues/98#issuecomment-169761955
cited from the link:
Internally at Airbnb, we use a pattern like the following:
class MyComponent extends React.Component {
...
}
export default connect(MyComponent); // default export. used in your app.
export { MyComponent as PureMyComponent}; // pure component. used in tests
This will work fine with redux's connect function, but won't work out of the box with the decorator syntax. You could open a pull request to redux to have the #connect decorator expose the underlying wrapped component as a static prop like UnderlyingComponent or something like that
Does that help at all?
This post has an update, see the first answer
So, first of all, I did search for similar problems (and found a few threads) but nothing solved my problem. I'm trying to use quasar framework for the first time and maybe I got lost somewhere in the namespaces or something.
So, first, some info :
+I don't have any error when compiling with ESLint
+I don't have any error in my javascript console at runtime
My problem is :
+My actions and mutation do save something in the store, but not where it should (see screenshot at end of post)
+My getter does not seems to work, and is displayed as "undefined" in the vue dev tool
My store is organized like that :
+store [folder]
+ index.js
+ app-utils [folder]
--+ index.js
--+ getters.js
--+ actions.js
--+ mutations.js
--+ state.js
Code of the root index.js :
import Vue from 'vue'
import Vuex from 'vuex'
import appUtils from './app-utils'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
appUtils
}
})
export default store
Then, in the folder 'app-utils' :
Code for index.js :
import state from './state'
import * as getters from './getters'
import * as mutations from './mutations'
import * as actions from './actions'
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
Code for state.js :
export default {
state: {
currentPageTitle: 'Hello'
}
}
Code for getters.js :
export const getPageTitle = (state) => {
console.log('GET TITLE: ' + state.currentPageTitle)
return state.currentPageTitle
}
Code for mutations.js :
export const setPageTitle = (state, newPageTitle) => {
console.log('MUTATION SET TITLE: ' + newPageTitle)
state.currentPageTitle = newPageTitle
}
export const deletePageTitle = (state) => {
console.log('MUTATION DELETE TITLE')
state.currentPageTitle = ''
}
Code for actions.js :
export const setPageTitle = (context, newPageTitle) => {
console.log('ACTION SET TITLE: ' + newPageTitle)
context.commit('setPageTitle', newPageTitle)
}
export const deletePageTitle = (context) => {
console.log('ACTION DELETE TITLE')
context.commit('deletePageTitle')
}
The code from where i am trying to access it (int the getPageTitle computed field):
<template>
<q-page>
<q-resize-observable #resize="onResize" /> TITLE : {{getPageTitle}}
<div class="row">
</div>
</q-page>
</template>
import { mapGetters, mapActions } from 'vuex'
export default {
data: () => ({
pageSize: {
height: 0,
width: 0
}
}),
mounted () {
this.setPageTitle('Template manager')
},
destroyed () {
this.deletePageTitle()
},
computed: {
...mapGetters('appUtils', [
'getPageTitle'
])
},
methods: {
...mapActions('appUtils', [
'setPageTitle',
'deletePageTitle'
]),
onResize (size) {
this.pageSize = size
}
}
}
</script>
<style>
</style>
Finaly, screenshot from the vue plugin , you can see the value has been set upon triggering the mounted() hook, but not in the 'state', and the getter is undefined.
Screenshot from the vue dev plugin
Your state object looks like this:
export default {
state: {
currentPageTitle: 'Hello'
}
}
And that whole thing is being passed to your getter state parameter. That entire exported object is your state, not the "state" property within it. So you have two options:
Option one: Update your getter to access the nested "state" property within your state:
export const getPageTitle = (state) => {
console.log('GET TITLE: ' + state.state.currentPageTitle)
return state.state.currentPageTitle
}
Option two (Probably what you really want to do): change your state object to not have the "state" property.
export default {
currentPageTitle: 'Hello'
}
Update: solved, see selected answer
So I solved my problem. It would appear thatthe first argument sent to my mutation is not the state, but the store itself. So :
This does not work
export const setPageTitle = (state, newPageTitle) => {
console.log('MUTATION SET TITLE: ' + newPageTitle)
state.currentPageTitle = newPageTitle
}
This works
export const setPageTitle = (store, newPageTitle) => {
console.log('MUTATION SET TITLE: ' + store.newPageTitle)
store.state.currentPageTitle = newPageTitle
}
Is this normal behaviour ? The doc seems to say the first argument is supposed to be the state itself.
There is a vue royter
.....
const Edit = Vue.component('edit', require('./components/pages/edit.vue'));
const Product_category = Vue.component('home', require('./components/pages/product_categories.vue'));
const routes = [
........
{
path: '/product_categories',
name: 'product_categories',
component: Product_category
},
{
path: '/product_categories/edit/:id',
name: 'product_categories_edit',
components: {default: Edit, entity: Product_category},
props: {default: true, entity: true}
}
];
How can I get data in component Product_category componate Edit?
<script>
export default {
props: ['id', 'entity'],
mounted: function () {
console.log('Admin edit page mounted.');
console.log(this.entity); // Eror
console.log(entity); // Eror
this.getData();
},
}
</script>
A direct appeal is not suitable, because each router will have its own entity.
Use vuex if you prefer a global state-object. It's properties can be mapped to each instance and component https://github.com/vuejs/vuex
If you prefer an event based approach use an event bus https://alligator.io/vuejs/global-event-bus/
It is all well described at multiple positions in the official vuejs documentation.