Access Vue3 global components - vue.js

So I'm trying to convert this Vue2 project to Vue3(typescript).
It's registering components globally and accessing them to match against a value in my store, however when trying to implement this in Vue3 the components stay undefined.
import getComponentTypeForContent from "../api/getComponentTypeForContent";
import { mapState } from "vuex";
import { defineComponent } from "vue";
export default defineComponent({
name: "PageComponentSelector",
beforeCreate: function () {
console.log("CREATED PAGECOMPONENTSELECTOR");
},
computed: mapState({
model: (state) => state.epiDataModel.model,
modelLoaded: (state) => state.epiDataModel.modelLoaded,
}),
methods: {
getComponentTypeForPage(model) {
// this.$options.components will contain all globally registered components from main.js
return getComponentTypeForContent(model, this.$options.components);
// this.$options.components fetches all components for vue2 app
},
},
});
and registering components like this:
//Pages
import LoginPage from "./components/pages/Login.vue";
const appAdv = createApp(App);
//Register components
appAdv.component("LoginPage", LoginPage);
appAdv.use(store).use(router).mount("#appAdv");
Can't find (or searching badly) how to do this or similar in vue3 so I've come here hoping someone could help hehe

I also frequently use parent components in child components, but don't (or haven't) used the App.component method
In vue3 I usually use the provide/inject . method
Provide
const app = createApp(App);
app.provide('someVarName', someVar); // `Provide` a variable to all components here
Inject:
// In *any* component
const { inject } = Vue;
...
setup() {
const someVar = inject('someVarName'); // injecting variable in setup
return {someVar}
}

Related

Can a Vue component/plugin have its own pinia state, so that multiple component instances don't share the same state

I have a "standalone" component which is set up as a Vue plugin (to be downloaded via npm and used in projects) and it uses pinia, but it looks like multiple instances of the component share the same pinia state. Is there a way to set up pinia such that each component instance has its own state?
The component is made up of multiple sub-(sub)-components and I'm using pinia to manage its overall state. Imagine something fairly complex like a <fancy-calendar /> component but you could have multiple calendars on a page.
I have the standard pinia set up in an index.js:
import myPlugin from "./myPlugin.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
export function myFancyPlugin(app, options) {
app.use(pinia);
app.component("myPlugin", myPlugin);
}
Then myPlugin.vue has:
<script setup>
import { useMyStore } from '#/myPlugin/stores/myStore'
import { SubComponent1 } from '#/myPlugin/components/SubComponent1'
import { SubComponent2 } from '#/myPlugin/components/SubComponent2'
...
const store = useMyStore()
The sub-components also import the store. Also some of the sub-components also have their own sub-components which also use the store.
myStore.js is set up like this:
import { defineStore } from "pinia";
export const useMyStore = defineStore("myStore", {
state: () => ({
...
}),
getters: {
...
},
actions: {
...
}
});
Edit: This is the solution I ended up using:
myStore.js:
import { defineStore } from "pinia"
export const useMyStore = (id) =>
defineStore(id, {
state: () => ({
...
}),
getters: {},
actions: {},
})();
myPlugin.vue
...
<script setup>
import { provide } from "vue"
import { useMyStore } from '#/MyNewPlugin/stores/MyStore'
import { v4 } from "uuid"
const storeId = v4()
provide('storeId', storeId)
const store = useMyStore(storeId)
...
SubComponent1.vue
<script setup>
import { inject } from "vue"
import { useMyStore } from '#/MyNewPlugin/stores/MyStore'
const storeId = inject('storeId')
const store = useMyStore(storeId)
</script>
A simple way of solving this is to create a stores map, using unique identifiers:
When you init a new instance of the root component of your plugin, you create a unique identifier for the current instance:
import { v4 } from 'uuid'
const storeId = v4();
You pass this id to its descendants via props or provide/inject.
Whenever a descendent component calls the store, it calls it with the storeId:
const store = useMyStore(storeId)
Finally, inside myStore:
const storesMap = {};
export const useMyStore = (id) => {
if (!storesMap[id]) {
storesMap[id] = defineStore(id, {
state: () => ({ ... }),
actions: {},
getters: {}
})
}
return storesMap[id]()
}
Haven't tested it, but I don't see why it wouldn't work.
If you need hands-on help, you'll have to provide a runnable minimal reproducible example on which I could test implementing the above.

Vuex 4 Modules can't use global axios property

I have a Vue3 (without Typescript) app running Vuex 4.0.0.
I'm trying to set up a simple GET request in one of my store modules using axios, however I'm getting a Cannot read property of 'get' of undefined when I try to do it via an action called in one of my components, however if I call this.$axios from the component it works fine. For some reason, my modules can't use this.$axios, while elsewhere in the app I can.
I've declared $axios as a globalProperty in my main.js file.
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
import { store } from "./store";
import axios from "axios";
const app = createApp(App).use(store).use(router);
app.config.globalProperties.$axios = axios;
app.mount("#app");
And the store module looks like this (simplified for the purposes of this question):
// store/modules/example.js
const state = () => ({
message: ""
});
const mutations = {
getMessage(state, payload) {
state.message = payload;
}
};
const actions = {
async setMessage(commit) {
this.$axios.get("example.com/endpoint").then(response => {
commit("setMessage", response.message);
});
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
The main store/index.js that's getting imported in main.js above looks like this:
// store/index.js
import "es6-promise";
import { createStore } from "vuex";
import example from "./modules/example";
export const store = createStore({ modules: { example } });
In the component, I have the following:
// MyComponent.vue
import { mapGetters, mapActions } from "vuex";
export default {
computed: {
...mapGetters({
message: "example/getMessage"
})
},
methods: {
...mapActions({
getMessage: "example/setMessage"
})
}
};
And a simple button with #click="getMessage". Clicking this button, however, returns Uncaught (in promise) TypeError: Cannot read property 'get' of undefined, but if I copy/paste the setMessage action into a component method, it works just fine.
Is there a way for me to expose this.$axios to the store files?
While this is not the ideal solution, as I would've preferred to have my $axios instance available globally with a single declaration in the mount file, it's probably the next best thing.
I made a lib/axiosConfig.js file that exports an axios instance with some custom axios options, and I just import that one instance in every module that needs it.
import axios from "axios";
axios.defaults.baseURL= import.meta.env.DEV ? "http://localhost:8000": "example.com";
axios.defaults.headers.common["Authorization"] = "Bearer " + localStorage.getItem("token");
axios.defaults.headers.common["Content-Type"] = "application/json";
// etc...
export const $axios = axios.create();
And in whatever module I need $axios in, I just import { $axios } from "./lib/axiosConfig. It's not perfect as I mentioned, since I do still have to import it in every module, but it's close enough as far as I can see, and has the added benefit of using the same axios config everywhere by just importing this file.

How to get access to the Root inside the setup method of Vue 3 component?

I have an Vue App.
I use vuex.
I created my app like this:
import { createApp } from "vue";
import axios from "axios";
import App from "./App.vue";
import router from "./router";
import store from "./store/index";
axios.defaults.baseURL = "https://localhost:44349";
const app = createApp(App)
.use(router)
.use(store)
.mount("#app");
Than i one of my component i am trying to access context.root.$store in the setup() method
, but context.root is undefined.
<script>
import {ref, computed } from "vue";
export default {
name: "ClientList",
setup(props, context) {
const clients = ref([]);
console.log(context);
const clientOrdersLenght = computed(() => {
return clients.value.length;
});
return { clients, clientOrdersLenght }
},
};
</script>
My idea is get acces to my store via context.root. I watched videos and examples with this. but they refer to Vue 2 using 'vue/composition-api' as import.
What i am missing?
You might be able to include and access the store directly
store.dispatch('myModule/myModuleAction')
store.myModule.state.blabla
import {ref, computed } from "vue";
import store from './store/index'; // include store
export default {
name: "ClientList",
setup(props) {
const clients = ref([]);
const clientOrdersLength = computed(() => {
store.dispatch('myAction'); // call dispatch
store.state.myItem // or access store's state
return clients.value.length;
});
return { clients, clientOrdersLength }
},
};
I found an easy way to access my store with getStore() from vuex. That's cool.
But for other reason in the feature, me or somebody else will need to access the $root (vue instance) of the app. So may be now the correct question is how to get the $root (vue instance) of the app in a child component?

vuex unknown action type when attempting to dispatch action from vuejs component

I'm using laravel, vue and vuex in another project with almost identical code and it's working great. I'm trying to adapt what I've done there to this project, using that code as boilerplate but I keep getting the error:
[vuex] unknown action type: panels/GET_PANEL
I have an index.js in the store directory which then imports namespaced store modules, to keep things tidy:
import Vue from "vue";
import Vuex from "vuex";
var axios = require("axios");
import users from "./users";
import subscriptions from "./subscriptions";
import blocks from "./blocks";
import panels from "./panels";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
},
actions: {
},
mutations: {
},
modules: {
users,
subscriptions,
blocks,
panels
}
})
panels.js:
const state = {
panel: []
}
const getters = {
}
const actions = {
GET_PANEL : async ({ state, commit }, panel_id) => {
let { data } = await axios.get('/api/panel/'+panel_id)
commit('SET_PANEL', data)
}
}
const mutations = {
SET_PANEL (state, panel) {
state.panel = panel
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
Below is the script section from my vue component:
<script>
import { mapState, mapActions } from "vuex";
export default {
data () {
return {
}
},
mounted() {
this.$store.dispatch('panels/GET_PANEL', 6)
},
computed:
mapState({
panel: state => state.panels.panel
}),
methods: {
...mapActions([
"panels/GET_PANEL"
])
}
}
</script>
And here is the relevant code from my app.js:
import Vue from 'vue';
import Vuex from 'vuex'
import store from './store';
Vue.use(Vuex)
const app = new Vue({
store: store,
}).$mount('#bsrwrap')
UPDATE:: I've tried to just log the initial state from vuex and I get: Error in mounted hook: "ReferenceError: panel is not defined. I tried creating another, very basic components using another module store, no luck there either. I checked my vuex version, 3.1.0, the latest. Seems to be something in the app.js or store, since the problem persists across multiple modules.
Once you have namespaced module use the following mapping:
...mapActions("panels", ["GET_PANEL"])
Where first argument is module's namespace and second is array of actions to map.

Why isn't router.currentRoute.path reactive?

I have an app which is contained in this div:
<div id="app" v-bind:style='{backgroundColor: backgroundColor}'>
... the app ...
</div>
The routing is done following the example in the documentation (this is a webpack project):
import Vue from 'vue/dist/vue.js'
import VueRouter from 'vue-router'
import ComponentOne from './component1.vue'
import ComponentTwo from './component2.vue'
Vue.use(VueRouter)
const routes = [{
path: '/foo',
component: ComponentOne
},
{
path: '/bar',
component: ComponentTwo
}
]
const router = new VueRouter({
routes // short for `routes: routes`
})
const app = new Vue({
router,
data: {
day: "Monday"
},
computed: {
backgroundColor: function () {
console.log(JSON.stringify(router.currentRoute))
if (router.currentRoute.path == "/foo") {
return "green"
} else {
return "blue"
}
}
}
}).$mount('#app')
I wanted the background to be dependent on the current route (router.currentRoute.path).
But, the solution above does not work, because router.currentRoute.path is not detected by the Vue instance as having changed (is not reactive).
What is the correct way to access the dynamic router data from within the Vue instance?
The router object created via new VueRouter is not reactive because Vue has no way to know to watch and update any object outside of its scope.
Passing router in the Vue config object is what allows the current route to be watched, but you need to reference it via this.$route:
if (this.$route.path == "/foo") {
...
}
You can also access the entire router object via this.$router, but its data is not reactive.
And if you are using Vue 2 with composition api setup() approach you can do this:
import { computed } from '#vue/composition-api'
export default {
setup (props, context) {
const params = computed ( () => context.root.$route.params)
const path = computed( () => context.root.$route.path)
I found on Vue's documentation page that tracks the router using watch for transition animations. Not sure if this is a best practice but you can use to.path or from.path to grab the path instead.
// then, in the parent component,
// watch the `$route` to determine the transition to use
watch: {
'$route': (to, from) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}