How to access Vuex from Vue Plugin? - vuejs2

How can I access my store from my plugin? Console returns undefined.
import store from './store';
export default {
install(vue, opts){
Vue.myGlobalFunction = function(){
console.log(store);
}
}
}

I recently had to do this too to make a pouchDb plugin, and came up with a new way.
When you create your first Vue object, you can do this.
import PouchDb from '#/pouch_db/PouchDbPlugin'
let DefaultVue = Vue.extend({
components: {App},
store,
created () {
Vue.use(PouchDb, this.$store) // Create it by passing in the store you want to use
}
})
My plugin adds an additional store, and it's own mutations and getters.
export default {
install (Vue, store) {
store.registerModule('PouchDb', pds)
const pouchDb = new PouchDb(store)
Vue.pouchDb = pouchDb
Vue.prototype.$pouchDb = pouchDb
}
}
Inside the constructor, I store the store
class PouchDb {
constructor (store) {
this.store = store
// ... etc.
}
// ... more functions
}
And then use it in other functions
class PouchDb {
// ... constructor and other functions
async addSync (docId) {
this.store.dispatch('PouchDb/addSync', docId)
}
}
It's a bit of a cheat to pass in the store, but seems to work nicely. It's usable throughout the app like this
// Inside vuex store
Vue.pouchDb.addSync(// ...etc)
// inside component
this.$pouchDb.removeSync(// ...etc)

See official guide here where it states
A Vue.js plugin should expose an install method. The method will be called with the Vue constructor as the first argument, along with possible options:
So you can do this, very easily.
Vue.use( {
install(Vue){
Vue.prototype.$something = function (){
this.$store...etc
}
}
} )
To use, simply do this.$something() in a components methods/computed etc, or directly in the component markup as {{$something()}}
This will remove the plugin needing to know where the store actually resides, while still allowing you to utilize the store within the plugin.
This is because it will inherit the scope of whatever component utilizes it, thus providing access to all of the components instance properties, including things like $store, $router as well any of it's local properties such as computed properties, parents etc. Essentially the plugin functions as if it is directly a part of the component (eg if you used it as a mixin).

For Vue 3
Incase if you wonder, how to do it in Vue 3, You can use the following.
plugin.js
export default {
install(app) { // app instance
console.log(app.config.globalProperties.$store)
}
}
main.js
import store from './pathtostore'
import plugin from './plugin'
createApp(...).use(store).use(plugin)

When app starts, you import your store and "append" it to Vue, globally.
Now, if you use() your plugin, the first parameter of install() is always Vue itself, and in this moment Vue already has access to the store, in the install method you can simply start
install(vue, opts) {
... here your can acces to vue.$store ....
}

Related

How to correctly set up Cypress 10, Vue2, Vuetify, Composition API for component testing?

The guide is quite confusing and obviously not correct when trying to set up Cypress 10 for component testing with Vue2 and Vuetify with composition API. There's lots of errors of unknown tags, things returned from setup() aren't accessible, spread operators where there shouldn't be, imports that don't work etc. What's the correct way to set things up so testing works?
You need to set up Vuetify as regular, to the global Vue object. Then in the mount you need to give the Vuetify object to the mount function so it can be found by the components. When using Composition API that also needs to be set up regularly to the global instance (unlike Vuetify it also works in the local instance, if you want).
Then mount the component inside a v-appso it should work properly and pass arugments around.
So component.ts file will include this:
import { mount } from 'cypress/vue2'
import Vuetify from 'vuetify'
import VueCompositionAPI from '#vue/composition-api';
import Vue from 'vue'
import { VApp } from 'vuetify/lib/components/VApp';
Vue.use(Vuetify);
Vue.use(VueCompositionAPI);
Cypress.Commands.add('mount', (component, args) => {
args.vuetify = new Vuetify(yourVuetifyOptions);
return mount({ render: (h) => h(VApp, [h(component, args)]) }, args);
})
When using the mount just do:
cy.mount(myComponent, { props: {someProp: 123 } });
If you need to set up plugins for the local Vue instance in the test they need to be set in args.extensions.plugins, the guide seems to mention globals but that is incorrect.
cy.mount(myComponent, { props: {someProp: 123 }, extensions: { plugins: [MyPlugin] } });
Note that I'm using args for both settings parameters for mount and also for the component, if needed those two can be separated. But there shouldn't be much clashing of properties and attributes so this works.
Also the props/attributes/etc for the component must be given as they're given to createElement, not mount (so props instead of propsData etc).

Nuxt class-based services architecture (register globally; vs manual import)

In my Nuxt app I'm registering app services in a plugin file (e.g. /plugins/services.js) like this...
import FeatureOneService from '#/services/feature-one-service.js'
import FeatureTwoService from '#/services/feature-two-service.js'
import FeatureThreeService from '#/services/feature-three-service.js'
import FeatureFourService from '#/services/feature-four-service.js'
import FeatureFiveService from '#/services/feature-five-service.js'
export default (ctx, inject) => {
inject('feature1', new FeatureOneService(ctx))
inject('feature2', new FeatureTwoService(ctx))
inject('feature3', new FeatureThreeService(ctx))
inject('feature4', new FeatureFourService(ctx))
inject('feature5', new FeatureFiveService(ctx))
}
After doing this I can access any of my service on vue instance like this.$feature1.someMethod()
It works but I've once concern, that is, this approach loads all services globally. So whatever page the user visits all these services must be loaded.
Now I've 20+ such services in my app and this does not seem optimal approach to me.
The other approach I was wondering is to export a singleton instance within each service class and import this class instance in any component which needs that service.
So basically in my service class (e.g. feature-one-service.js) I would do like to do it like this..
export default new FeatureOneService() <---- I'm not sure how to pass nuxt instance in a .js file?
and import it my component where it is required like so...
import FeatureOneService from '#/services/feature-one-service.js'
What approach do you think is most feasible? if its the second one, then how to pass nuxt instance to my singleton class?
Yep, loading everything globally is not optimal in terms of performance.
You will need to either try to use JS files and pass down the Vue instance there.
Or use mixins, this is not optimal but it is pretty much the only solution in terms of reusability with Vue2.
Vue3 (composition API) brings composables, which is a far better approach regarding reusability (thing React hooks).
I've been struggling a lot with it and the only solution is probably to inject services to the global Vue instance at the component/page level during the initialisation (in created hook), another option is to do that in the middleware (or anywhere else where you have access to the nuxt context. Otherwise you won't be able to pass nuxt context to the service.
I usually set up services as classes, call them where necessary, and pass in the properties of the context which the class depends on as constructor arguments.
So for example, a basic MeiliSearchService class might look like:
export class MeilisearchService {
#client: MeiliSearch
constructor($config: NuxtRuntimeConfig) {
super()
this.#client = new MeiliSearch({
host: $config.services.meilisearch.host,
apiKey: $config.services.meilisearch.key
})
}
...
someMethod() {
let doSomething = this.#client.method()
...
}
...
}
Then wherever you need to use the service, just new up an instance (passing in whatever it needs) and make it available to the component.
data() {
const meiliSearchService = new MeiliSearchService(this.$config)
return {
meiliSearchService,
results,
...
}
},
methods: {
search(query) {
...
this.results = this.meiliSearchService.search(query)
...
}
}
As I'm sure you know, some context properties are only available in certain Nuxt life-cycle hooks. I find most of what I need is available everywhere, including $config, store, $route, $router etc.
Don't forget about Vue's reactivity when using this approach. For example, using a getter method on your service class will return the most recent value only when explicitly called. You can't, for example, stick the getter method in a computed() property and expect reactivty.
<div v-for='result in latestSearchResults'>
...
</div>
...
computed: {
latestSearchResults() {
return this.#client.getLatestResults()
}
}
Instead, call the method like:
methods: {
getLatestResults() {
return this.#client.getLatestResults()
}
}

Vuex state getter not working inside the vue 3 composition api

Trying to implement the new Vue composition API I ran into an issue, my vuex state values are not working and it will return an $store undefined error. When I check if my language state is set it is set so theres data present. I could not find any good information about this but I did find some helper plugin but I dont want to use plugins for every issue so is there a way to do this?
I am using vue 3 with vuex 4
export default {
setup (props, context) {
console.log(context.root.$store.getters['i18n/language'])//not working
// more logic here
}
}
You should use the composable function called useStore to get the store instance :
import {useStore} from 'vuex'
export default {
setup (props, context) {
const store=useStore()
console.log(store.getters['i18n/language'])
}
}

How to integrate inertiaJS with quasar framework?

I would like to integrate intertiaJS into my Quasar app so that I can communicate with my Laravel backend. My problem now is that the general stuff is taken over by the Quasar CLI, which is good in principle, but in this case it takes away my entry point as described at https://inertiajs.com/client-side-setup:
import { createApp, h } from 'vue'
import { App, plugin } from '#inertiajs/inertia-vue3'
const el = document.getElementById('app')
createApp({
render: () => h(App, {
initialPage: JSON.parse(el.dataset.page),
resolveComponent: name => require(`./Pages/${name}`).default,
})
}).use(plugin).mount(el)
My thought is that I could use a boot file like the offered in Quasar (https://quasar.dev/quasar-cli/boot-files), but I have to admit that I don't have the right approach.
When I look at the app.js that is automatically generated, I see that nothing special happens in the rendering:
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.conf.js > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import Vue from 'vue'
import './import-quasar.js'
import App from 'app/src/App.vue'
import createStore from 'app/src/store/index'
import createRouter from 'app/src/router/index'
export default async function () {
// create store and router instances
const store = typeof createStore === 'function'
? await createStore({Vue})
: createStore
const router = typeof createRouter === 'function'
? await createRouter({Vue, store})
: createRouter
// make router instance available in store
store.$router = router
// Create the app instantiation Object.
// Here we inject the router, store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
store,
render: h => h(App)
}
app.el = '#q-app'
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}
I.e. in principle I should be able to link in without it causing any conflict situations. The question is, how would that look?
I have to link into the rendering afterwards and overwrite it as described in the code example. I would like to stay with the Quasar Cli, because it is very useful and the situation described here is the only exception.
p7
the boot files is the right place to inject and initialize your own dependencies or just configure some startup code for your application.
I have not had the opportunity to use the library you mention, but I detail a little how you could implement
create your boot file
import { plugin } from '#inertiajs/inertia-vue';
export default async({ app, Vue }) => {
Vue.use(plugin);
}
until there you have 50%. On the other hand, you cannot do a mixin to the main instance but you could do it for each page, however I recommend that you make a component part to which you add the data you need and make a mixin of the library you need
<template>
<div />
</template>
<script>
import { App } from '#inertiajs/inertia-vue';
export default {
mixins: [App],
props: ['initialPage', 'resolveComponent'],
}
</script>
In order to do this, modify according to how the library you use works.

Access data model in VueJS with Cypress (application actions)

I recently came across this blog post: Stop using Page Objects and Start using App Actions. It describes an approach where the application exposes its model so that Cypress can access it in order to setup certain states for testing.
Example code from the link:
// app.jsx code
var model = new app.TodoModel('react-todos');
if (window.Cypress) {
window.model = model
}
I'd like to try this approach in my VueJS application but I'm struggling with how to expose "the model".
I'm aware that it's possible to expose the Vuex store as described here: Exposing vuex store to Cypress but I'd need access to the component's data().
So, how could I expose e.g. HelloWorld.data.message for being accessible from Cypress?
Demo application on codesandbox.io
Would it be possible via Options/Data API?
Vue is pretty good at providing it's internals for plugins, etc. Just console.log() to discover where the data sits at runtime.
For example, to read internal Vue data,
either from the app level (main.js)
const Vue = new Vue({...
if (window.Cypress) {
window.Vue = Vue;
}
then in the test
cy.window().then(win => {
const message = win.Vue.$children[0].$children[0].message;
}
or from the component level
mounted() {
if (window.Cypress) {
window.HelloWorld = this;
}
}
then in the test
cy.window().then(win => {
const message = win.HelloWorld.message;
}
But actions in the referenced article implies setting data, and in Vue that means you should use Vue.set() to maintain observability.
Since Vue is exposed on this.$root,
cy.window().then(win => {
const component = win.HelloWorld;
const Vue = component.$root;
Vue.$set(component, 'message', newValue);
}
P.S. The need to use Vue.set() may go away in v3, since they are implementing observability via proxies - you may just be able to assign the value.
Experimental App Action for Vue HelloWorld component.
You could expose a setter within the Vue component in the mounted hook
mounted() {
this.$root.setHelloWorldMessage = this.setMessage;
},
methods: {
setMessage: function (newValue) {
this.message = newValue;
}
}
But now we are looking at a situation where the Cypress test is looking like another component of the app that needs access to state of the HelloWorld.
In this case the Vuex approach you referenced seems the cleaner way to handle things.