Accessing store from Quasar boot file - vue.js

I am using Quasar and I have in a boot file (basically same as main.js):
Vue.use(VueCurrencyFilter, {
symbol: '$',
thousandsSeparator: '.',
fractionCount: 2,
fractionSeparator: ',',
symbolPosition: 'front',
symbolSpacing: true
})
I can do in my component:
{{purchaseOrderTotal | currency(currentLocation.currency)}}
currentLocation is a state in my store called with mapState.
currentLocation.currency is the currency for symbol option.
This works but means I have to pass the param in every component.
Is there any way to call currentLocation.currency in the js file to be used as default?

I forgot about anatomy of boot files in Quasar:
import VueCurrencyFilter from 'vue-currency-filter'
export default ({ Vue, store }) => {
Vue.use(VueCurrencyFilter,
.......
};
store needed to be exported instead of imported.

You can import the Vuex store into any module:
import store from '#/store';
And use it in that file the same as though you were using it in a component:
let currency = store.state.currentLocation.currency;
This is the same this.$store object that's accessible through the components.

In Quasar 2 an alternative is to create a new boot file: quasar new boot file
In the created file ../boot/file.(js|ts) we define the variable: mystore (Or also myrouter if we want to use store and router in your functions)
And we initialize them from the "export default":
import { boot } from 'quasar/wrappers'
let myrouter = null;
let mystore = null;
/**
* #param {Array} value
*/
const redirectFromLogin = (value) => {
if (value.length > 1) {
//set routing
myrouter.push("/multiple");
} else {
//set routing
myrouter.push("/single");
}
}
/**
* #param {Array} userdata
*/
const saveUser = (userdata) => {
// Save user info in Store
mystore.commit("userstore/setUser", userdata);
}
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({ app, router, store }) => {
myrouter = router;
mystore = store
})
export { saveUser, redirectFromLogin }

Related

Testing custom hooks that are using next-i18next useTranslation hook

I have created a custom hook that returns the translated value using the useTranslation hook.
import { useTranslation } from "next-i18next";
export const useCustomHook = (data) => {
const {t, i18n: { language: locale }} = useTranslation();
const value = {
field: t("some.key.from.json.file", { arg: data.arg }),
field2: data.name,
field3: t("another.key", {
arg: data.arg2, count: 3
})
}
return value;
};
I want to create a unit test for this custom hook, but I can't get the useTranslation hook to work as it does when running the app itself. Further info my current setup is as follows:
1- I'm using Nextjs with next-i18next library.
2- No i18n provider to wrap the app, only using HOC from next-i18next to wrap _app.
3- I have 2 json files for locales.
Is there a way to allow the useTranslation hook to work and get the parsed value from the translation file? here's what I tried so far too:
1- mocking the useTranslation hook, but this returns the ("another.key") as is without the parsed value.
2- I tried to create a wrapper with i18n-next provider, but that didn't work too.
Here's my test file.
describe("useCustomHook()", () => {
it("Should return correctly mapped props", () => {
const { result } = renderHook(() =>
useCustomHook(mockData)
);
const data = result.current[0];
expect(data.field).toBe(mockData.field); // this returns ("some.key.from.json.file") it doesn't use the t function,
// ... //
});

How to pass an argument to Pinia store?

I'm making a session API call in main.js and using values from the response as the initial value for my root store. In vuex it's handled this like,
DataService.getSession()
.then((sessionData) => {
new Vue({
i18n,
router,
// this params sessionData.session will be passed to my root store
store: store(sessionData.session),
render: (h) => h(App),
}).$mount('#app');
})
Consumed like,
export default function store(sessionData) { // here I'm getting the sessionData
return new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
state: {
// some states here
},
});
}
In case of Pinia we're creating a app instance & making it use like,
app.use(createPinia())
And my store would be like,
// how to get that sessionData here
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
counter: 0
})
})
Is it possible to pass the sessionData someway to the pinia store?
There are 3 ways to pass parameters to a Pinia store - see the list below. You could use either #2 or #3 .
In most cases it is wise to initialise your Vue instance and show the user something while they are waiting for connections to resolve. So you may find it simpler to just access or initialise the store by calling DataService.getSession() in say a "SessionStore" action which can be async. Typically Components =access=> Stores =access=> Services.
Unlike Vuex, you don't need a root Pinia store. You can get just call useSomeStore() in the setup method for any component. Each store can just be an island of data. Pinia stores can reference other pinia store instances. This might be particularly useful if you're migrating a set of Vuex stores to Pinia and need to preserve the old Vuex tree of stores.
1. Pass common params to every action.
export const useStore = defineStore('store1', {
state: () => ({
...
}),
actions: {
action1(param1: string ... ) {
// use param1
}
}
});
2. Initialise store AFTER creating it
Only works if there's one instance of this store required
export const useStepStore = defineStore('store2', {
state: () => ({
param1: undefined | String,
param2: undefined | String,
...
}),
getters: {
getStuff() { return this.param1 + this.param2; }
}
actions: {
init(param1: string, param2: string) {
this.param1 = param1
this.param2 = param2
},
doStuff() {
// use this.param1
}
}
});
3. Use the factory pattern to dynamically create store instances
// export factory function
export function createSomeStore(storeId: string, param1: string ...) {
return defineStore(storeId, () => {
// Use param1 anywhere
})()
}
// Export store instances that can be shared between components ...
export const useAlphaStore = createSomeStore('alpha', 'value1');
export const useBetaStore = createSomeStore('beta', 'value2');
You could cache the session data in your store, and initialize the store's data with that:
In your store, export a function that receives the session data as an argument and returns createPinia() (a Vue plugin). Cache the session data in a module variable to be used later when defining the store.
Define a store that initializes its state to the session data cached above.
In main.js, pass the session data to the function created in step 1, and install the plugin with app.use().
// store.js
import { createPinia, defineStore } from 'pinia'
1️⃣
let initData = null
export const createStore = initStoreData => {
initData = { ...initStoreData }
return createPinia()
}
export const useUserStore = defineStore('users', {
state: () => initData, 2️⃣
})
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from './store'
import * as DataService from './data-service'
DataService.getSession().then(sessionData => {
createApp(App)
.use(createStore(sessionData)) 3️⃣
.mount('#app')
})
demo
When you create a store in Pinia using defineStore() you give it the initial state. So wherever you do that just pass the data into it and then do
defineStore('name', {
state: () => {
isAdmin: session.isAdmin,
someConstant: 17
},
actions: { ... }
});

"[vuex] state field foo was overridden by a module with the same name at foobar" with deepmerge helper function in jest

I'm using a helper function to create a store inside my jests. The helper function uses deepmerge to merge the basic configuration with a customized configuration. This results in multiple console warnings
[vuex] state field "cart" was overridden by a module with the same name at "cart"
[vuex] state field "customer" was overridden by a module with the same name at "customer"
[vuex] state field "checkout" was overridden by a module with the same name at "checkout"
store.js (Reduced to a minimum for presentation purpose)
import cart from './modules/cart'
import checkout from './modules/checkout'
import customer from './modules/customer'
Vue.use(Vuex)
export const config = {
modules: {
cart,
customer,
checkout,
},
}
export default new Vuex.Store(config)
test-utils.js
import merge from 'deepmerge'
import { config as storeConfig } from './vuex/store'
// merge basic config with custom config
export const createStore = config => {
const combinedConfig = (config)
? merge(storeConfig, config)
: storeConfig
return new Vuex.Store(combinedConfig)
}
making use of the helper function inside
somejest.test.js
import { createStore } from 'test-utils'
const wrapper = mount(ShippingComponent, {
store: createStore({
modules: {
checkout: {
state: {
availableShippingMethods: {
flatrate: {
carrier_title: 'Flat Rate',
},
},
},
},
},
}),
localVue,
})
How do I solve the console warning?
I believe the warning is somewhat misleading in this case. It is technically true, just not helpful.
The following code will generate the same warning. It doesn't use deepmerge, vue-test-utils or jest but I believe the root cause is the same as in the original question:
const config = {
state: {},
modules: {
customer: {}
}
}
const store1 = new Vuex.Store(config)
const store2 = new Vuex.Store(config)
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#3.4.0/dist/vuex.js"></script>
There are two key parts of this example that are required to trigger the warning:
Multiple stores.
A root state object in the config.
The code in the question definitely has multiple stores. One is created at the end of store.js and the other is created by createStore.
The question doesn't show a root state object, but it does mention that the code has been reduced. I'm assuming that the full code does have this object.
So why does this trigger that warning?
Module state is stored within the root state object. Even though the module in my example doesn't explicitly have any state it does still exist. This state will be stored at state.customer. So when the first store gets created it adds a customer property to that root state object.
So far there's no problem.
However, when the second store gets created it uses the same root state object. Making a copy or merging the config at this stage won't help because the copied state will also have the customer property. The second store also tries to add customer to the root state. However, it finds that the property already exists, gets confused and logs a warning.
There is some coverage of this in the official documentation:
https://vuex.vuejs.org/guide/modules.html#module-reuse
The easiest way to fix this is to use a function for the root state instead:
state: () => ({ /* all the state you currently have */ }),
Each store will call that function and get its own copy of the state. It's just the same as using a data function for a component.
If you don't actually need root state you could also fix it by just removing it altogether. If no state is specified then Vuex will create a new root state object each time.
It is logged when a property name within the state conflicts with the name of a module, like so:
new Vuex.Store({
state: {
foo: 'bar'
},
modules: {
foo: {}
}
})
therefore this raises the warning.
new Vuex.Store(({
state: {
cart: '',
customer: '',
checkout: ''
},
modules: {
cart: {},
customer: {},
checkout: {},
}
}))
its most likely here
export const createStore = config => {
const combinedConfig = (config)
? merge(storeConfig, config)
: storeConfig
return new Vuex.Store(combinedConfig)
}
from the source code of vuex, it helps indicate where these errors are being raised for logging.
If you run the app in production, you know that this warning wont be raised... or you could potentially intercept the warning and immediately return;
vuex source code
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
vuex tests
jest.spyOn(console, 'warn').mockImplementation()
const store = new Vuex.Store({
modules: {
foo: {
state () {
return { value: 1 }
},
modules: {
value: {
state: () => 2
}
}
}
}
})
expect(store.state.foo.value).toBe(2)
expect(console.warn).toHaveBeenCalledWith(
`[vuex] state field "value" was overridden by a module with the same name at "foo.value"`
)
Well, I believe there is no need for using deepmerge in test-utils.ts. It is better we use Vuex itself to handle the merging of the module instead of merging it with other methods.
If you see the documentation for Vuex testing with Jest on mocking modules
you need to pass the module which is required.
import { createStore, createLocalVue } from 'test-utils';
import Vuex from 'vuex';
const localVue = createLocalVue()
localVue.use(Vuex);
// mock the store in beforeEach
describe('MyComponent.vue', () => {
let actions
let state
let store
beforeEach(() => {
state = {
availableShippingMethods: {
flatrate: {
carrier_title: 'Flat Rate',
},
},
}
actions = {
moduleActionClick: jest.fn()
}
store = new Vuex.Store({
modules: {
checkout: {
state,
actions,
getters: myModule.getters // you can get your getters from store. No need to mock those
}
}
})
})
});
whistle, In test cases:
const wrapper = shallowMount(MyComponent, { store, localVue })
Hope this helps!

Unable to access vuex-orm $db variable from store in nuxt

Using nuxtjs with vuexorm & vuexorm-axios plugin.
/pages/index.vue
computed: {
users() {
// this works as expected
return User.all()
}
}
plugins/vuex-orm-axios.js
import { Model } from '#vuex-orm/core'
export default ({ $axios }) => {
Model.setAxios($axios)
}
store/index.js
import VuexORM from '#vuex-orm/core'
import VuexORMAxios from '#vuex-orm/plugin-axios'
import User from '#/models/user'
VuexORM.use(VuexORMAxios)
// Create a new database instance.
const database = new VuexORM.Database()
// Register Models to the database.
database.register(User)
export const plugins = [
VuexORM.install(database)
]
Above all works. But in vuexorm docs it says to always fetch model from injected database instance for nuxt/ssr apps.
But if I try to access the $db variable from store it wont work as there is no $db variable inside store.
/pages/index.vue
computed: {
users() {
// this wont work as $db is undefined
User () {
return this.$store.$db().model('users')
},
users () {
return this.User.all()
}
}
What am I doing wrong here?
In your store/index.js you have to export state to activate Vuex store. Add this to the file:
export const state = () => ({})

Not able to access i18 plugin from mutation in classic mode store in Nuxt application

I'm trying to implement Vuex i18n package within my Nuxt application. In my nuxt.conf.js file in plugins array I have:
{
src: '#/plugins/i18n.js',
ssr: false
},
plugins/i18n.js file is:
import Vue from "vue";
import vuexI18n from "vuex-i18n/dist/vuex-i18n.umd.js";
import toEnglish from "../translations/toEnglish";
import toSpanish from "./../translations/toSpanish";
import toGerman from "./../translations/toGerman";
export default ({ store }) => {
Vue.use(
vuexI18n.plugin,
store,
{
onTranslationNotFound: function (locale, key) {
console.warn(`vuex-i18n :: Key '${key}' not found for locale '${locale}'`)
}
}
);
// register the locales
Vue.i18n.add('en', toEnglish);
Vue.i18n.add('de', toGerman);
Vue.i18n.add('es', toSpanish);
// Set the start locale to use
Vue.i18n.set('de');
Vue.i18n.fallback('en');
}
Last thing is my store. I'm using classic mode of vuex store in Nuxt:
import Vuex from "vuex";
const store = () => {
return new Vuex.Store({
state: () => ({
currentLanguage: ''
}),
mutations: {
changeLang(state, response) {
if (response) {
console.log(this);
state.currentLanguage = response;
this.i18n.set(response);
}
}
}
})
};
export default store;
As you can see in store file in mutation I'm trying to access i18n plugin with this keyword. Unfortunetally in print error in console:
TypeError: Cannot read property 'set' of undefined
this which I consoled also inside mutation is:
I changed this.i18n.set(response); to state.i18n.locale = response; inside my mutation and now it seems working.
For some reason when I call this mutation my video.js player refresh. I will try to find out why.