Set global variables in vue 3 - vue.js

As u will see in the code, i am following vue 3 documentation on how to use global variables in vue 3, https://vuejs.org/api/application.html. This is main.js file:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css';
import 'jquery/src/jquery.js'
let app = createApp(App)
/* GLOBAL VARIABLES*/
// app.config.globalProperties.$headingMain = 'Direct mail marketing that works'
app.provide('headingMain', 'Direct mail marketing that works')
createApp(App).use(router).mount('#app')
import 'bootstrap/dist/js/bootstrap.js'
Home.vue component:
<template>
<h1 class="heading">{{ myHeading }}</h1>
</template>
<script>
export default {
inject: ["headingMain"],
data() {
return {
myHeading: this.headingMain,
};
},
};
</script>
h1 element wont show here.
This is approach with app.provide and inject. I also tried with app.config.globalProperties, you can see there is a commented line in main.js

// 1. Assign app to a variable
let app = createApp(App)
// 2. Assign the global variable before mounting. (here $globalVar is the name of your global variable. you should use the global name that starts with the '$' sign. but it's not compulsory.)
app.config.globalProperties.$globalVar = 'globalVar'
// 3. Use router and mount app
app.use(router).mount('#app')
if you want to use it in the script then in your component.
<script>
export default {
data() {
return {
myVar: this.$globalVar
}
}
}
</script>
if you want direcly in template than use like,
<template>
<h1>{{ $globalVar }}</h1>
</template>

The problem is you have two application instances, and you used provide on one instance while inject-ing from the other instance's child component. The two instances are not connected in any way.
// app instance 1
let app = createApp(App)
app.provide('headingMain', 'Direct mail marketing that works')
// app instance 2 (where the `inject` occurs)
createApp(App).use(router).mount('#app')
Solution
Use the same application instance to provide and inject:
let app = createApp(App)
app.provide('headingMain', 'Direct mail marketing that works')
👇
app.use(router).mount('#app')
demo

Related

Share global pinia store between components

I have two vue components with own loaders, mounted into two already rendered DOM nodes:
Component A:
import { createApp } from 'vue'
import ComponentA from '#/Vue/ComponentA.vue';
import {createPinia} from 'pinia';
createApp(ComponentA).use(createPinia()).mount(document.querySelector('.c-component-a'));
Component B:
import { createApp } from 'vue'
import ComponentB from '#/Vue/ComponentB.vue';
import {createPinia} from 'pinia';
createApp(ComponentA).use(createPinia()).mount(document.querySelector('.c-component-b'));
Now, I want to load a global pinia store into multiple components:
Pinia store:
import {defineStore} from 'pinia';
export type RootState = {
foobar: number;
}
export const useGlobalApplicationStore = defineStore({
id: 'global',
state: () => ({
foobar: 100
} as RootState),
actions: {
setFoobar(payload: number): void {
this.foobar = payload;
}
},
getters: {
getFoobar(state: RootState): number {
return state.foobar;
}
}
})
If component A sets a value in this store, component B should react to changes.
Component A:
const globalApplicationStore = useGlobalApplicationStore();
setTimeout(() => {
globalApplicationStore.setFoobar(400);
}, 2000);
Output of {{globalApplicationStore.foobar}} in component A changes from 100 to 400 after 2 seconds, as expected.
Component B:
const globalApplicationStore = useGlobalApplicationStore();
Output of {{globalApplicationStore.foobar}} in component B does not change from 100 to 400.
I guess, both components loads the store as local instances.
How can I share a store between seperate mounted components?
After a long search I found out that it's pretty easy (as often...).
In my case, I use the progressive aspect of Vue.js to put apps in different places of my HTML code. Specifically, I want to populate a shopping cart icon in the header of my layout with the number of items. So I am using a App.vue for my product-app and a Basket.vue for my basket-indicator.
The simple trick is to instantiate pinia just once. Let's say you have a main.js as an entry-point of your app:
import { createApp } from "vue";
import App from "./App.vue";
import Basket from "./Basket.vue";
import {createPinia} from 'pinia';
const pinia = createPinia();
// Init App
createApp(App)
.use(pinia)
.mount("#app");
// Init Basket
createApp(Basket)
.use(pinia)
.mount("#basket");
In your App.vue you just import your stores (in my case a product store and a cart store).
<script setup>
... import components ...
import {useProductStore} from "#/stores/ProductStore";
import {useCartStore} from "#/stores/CartStore";
const productStore = useProductStore();
const cartStore = useCartStore();
productStore.fill();
</script>
<template>
... your components ...
</template>
the same in your Basket.vue:
<script setup>
import CartWidget from "#/components/CartWidget.vue";
import {useCartStore} from "#/stores/CartStore";
import {useProductStore} from "#/stores/ProductStore";
const cartStore = useCartStore();
const productStore = useProductStore();
productStore.fill();
</script>
<template>
<div class="container">
<CartWidget/>
</div>
</template>
That's it.
"pinia": "^2.0.17",
"vue": "^3.2.39"

Vue 3: component `:is` in for loop fails

I'm trying to loop over a list of component described by strings (I get the name of the component from another , like const componentTreeName = ["CompA", "CompA"].
My code is a simple as:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="compName"></component>
</template>
</template>
Demo here
EDIT
I tried this with not much success.
Use resolveComponent() on the component name to look up the global component by name:
<script setup>
import { resolveComponent, markRaw } from 'vue'
const myGlobalComp = markRaw(resolveComponent('my-global-component'))
</script>
<template>
<component :is="myGlobalComp" />
<template>
demo 1
If you have a mix of locally and globally registered components, you can use a lookup for local components, and fall back to resolveComponent() for globals:
<script setup>
import LocalComponentA from '#/components/LocalComponentA.vue'
import LocalComponentB from '#/components/LocalComponentB.vue'
import { resolveComponent, markRaw } from 'vue'
const localComponents = {
LocalComponentA,
LocalComponentB,
}
const lookupComponent = name => {
const c = localComponents[name] ?? resolveComponent(name)
return markRaw(c)
}
const componentList = [
'GlobalComponentA',
'GlobalComponentB',
'LocalComponentA',
'LocalComponentB',
].map(lookupComponent)
</script>
<template>
<component :is="c" v-for="c in componentList" />
</template>
demo 2
Note: markRaw is used on the component definition because no reactivity is needed on it.
When using script setup, you need to reference the component and not the name or key.
To get it to work, I would use an object where the string can be used as a key to target the component from an object like this:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
const components = {CompA};
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="components[compName]"></component>
</template>
</template>
To use a global component, you could assign components by pulling them from the app context. But this would require the app context to be available and the keys known.
example:
import { app } from '../MyApp.js'
const components = {
CompA: app.component('CompA')
}
I haven't tested this, but this might be worth a try to check with getCurrentInstance
import { ref,getCurrentInstance } from 'vue'
const components = getCurrentInstance().appContext.components;

Vue: When to register component in main.js vs in the .vue file

Say I have a basic vue project
main.js:
import { createApp } from 'vue';
import App from './App.vue';
import SomeComponent from './components/SomeComponent.vue';
const app = createApp(App);
app.component('some-component', SomeComponent);
app.mount('#app');
components/App.vue:
<template>
<some-component></some-component>
</template>
<script>
import SomeComponent from "./SomeComponent.vue";
export default {
components: { SomeComponent },
};
</script>
components/SomeComponent.vue:
<template>
<div></div>
</template>
<script>
export default {};
</script>
Note that in main.js, I call app.component('some-component', SomeComponent); and in SomeComponent.vue I also specify the same with components: { SomeComponent },. Only one of the two ways is needed (though specifying both doesn't seem to cause errors).
My question is this: When would you specify components in main.js instead of in the component that will actually use it?
It seems that I could create an entire storefront and list all of the components in main.js without ever using components: {} inside a single one of my components and it would work. But it seems more logical to me to list the used sub-components inside each component that will use them for the encapsulation and reusability it brings. But that's because I have an object oriented mindset.

Vue component opened in Ionic Vue Modal doesn't have $route and $router

I'm creating app by using Vue Ionic and trying to change page by modal controller provided by ionic like the following code.
const modal = await this.$ionic.modalController.create({
component: NewPageComponent
})
modal.present()
Then NewPageComponent is opened without problem in <ion-modal></ion-modal>.
But when I try to confirm $route and $router in NewPageComponent, NewPageComponent doesn't have these.
NewPageComponent
export default {
created () {
console.log(this.$route) // undefined
console.log(this.$router) // undefined
}
}
Vue Components opened in <ion-modal></ion-modal> don't seem to have $route and $router.
But others have these without problem.
For example I can see $route and $router in App.vue.
App.vue
<template>
<ion-app>
<v-app>
<span>App</span>
</v-app>
<ion-app>
</template>
<script>
export default {
created () {
console.log(this.$route) // this.$route exists
console.log(this.$router) // this.$router exists
}
}
</script>
Of course I have registered router in main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ionic from '#ionic/vue'
import './registerServiceWorker'
import './plugins/ionic.js'
Vue.use(Ionic)
Vue.config.productionTip = false
new Vue({
router, // I have set router like this
render: h => h(App)
}).$mount('#app')
Is there any way to make Vue Components opened in <ion-modal></ion-modal> like NewPageComponent have $route and $router?
Internally Ionic uses another Vue instance. When you creating new modal it is placed in this instance without router that is registered in main app. I recommend you to use event bus pattern to communicate between components. It's pretty simple:
EventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
Then in main app you can listen to custom events:
App.vue
import {EventBus} from './EventBus.js'
//...
created() {
EventBus.$on('change-route', this.handleRouteChange)
EventBus.$on('change-params', this.handleParamsChange)
},
beforeDestroy() {
EventBus.$off('change-route', this.handleRouteChange)
EventBus.$off('change-params', this.handleParamsChange)
},
methods: {
handleRouteChange(page) {
// here you can normally access this.$router
this.$router.push(page)
},
handleParamsChange(params) {
this.$router.push({params:params})
}
},
NewPageComponent.vue
import {EventBus} from './EventBus.js'
//...
methods: {
changePage() {
EventBus.$emit('change-page', '/new-page')
// If you want to change page named route with params:
// EventBus.$emit('change-page', {name:'product', params: {id: 10}})
},
updateParams() {
EventBus.$emit('change-params', {id: 1010})
}
}

Vue.js - Making helper functions globally available to single-file components

I have a Vue 2 project that has many (50+) single-file components. I use Vue-Router for routing and Vuex for state.
There is a file, called helpers.js, that contains a bunch of general-purpose functions, such as capitalizing the first letter of a string. This file looks like this:
export default {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
My main.js file initializes the app:
import Vue from 'vue'
import VueResource from "vue-resource"
import store from "./store"
import Router from "./router"
import App from "./components/App.vue"
Vue.use(VueResource)
const app = new Vue({
router: Router,
store,
template: '<app></app>',
components: { App },
}).$mount('#app')
My App.vue file contains the template:
<template>
<navbar></navbar>
<div class="container">
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {
// stuff
}
}
}
</script>
I then have a bunch of single-file components, which Vue-Router handles navigating to inside the <router-view> tag in the App.vue template.
Now let's say that I need to use the capitalizeFirstLetter() function inside a component that is defined in SomeComponent.vue. In order to do this, I first need to import it:
<template>Some Component</template>
<script>
import {capitalizeFirstLetter} from '../helpers.js'
export default {
data() {
return {
myString = "test"
}
},
created() {
var newString = this.capitalizeFirstLetter(this.myString)
}
}
</script>
This becomes a problem quickly because I end up importing the function into many different components, if not all of them. This seems repetitive and also makes the project harder to maintain. For example if I want to rename helpers.js, or the functions inside it, I then need to go into every single component that imports it and modify the import statement.
Long story short: how do I make the functions inside helpers.js globally available so that I can call them inside any component without having to first import them and then prepend this to the function name? I basically want to be able to do this:
<script>
export default {
data() {
return {
myString = "test"
}
},
created() {
var newString = capitalizeFirstLetter(this.myString)
}
}
</script>
inside any component without having to first import them and then prepend this to the function name
What you described is mixin.
Vue.mixin({
methods: {
capitalizeFirstLetter: str => str.charAt(0).toUpperCase() + str.slice(1);
}
})
This is a global mixin. with this ALL your components will have a capitalizeFirstLetter method, so you can call this.capitalizeFirstLetter(...) from component methods or you can call it directly as capitalizeFirstLetter(...) in component template.
Working example: http://codepen.io/CodinCat/pen/LWRVGQ?editors=1010
See the documentation here: https://v2.vuejs.org/v2/guide/mixins.html
Otherwise, you could try to make your helpers function a plugin:
import Vue from 'vue'
import helpers from './helpers'
const plugin = {
install () {
Vue.helpers = helpers
Vue.prototype.$helpers = helpers
}
}
Vue.use(plugin)
In your helper.js export your functions, this way:
const capFirstLetter = (val) => val.charAt(0).toUpperCase() + val.slice(1);
const img2xUrl = (val) => `${val.replace(/(\.[\w\d_-]+)$/i, '#2x$1')} 2x`;
export default { capFirstLetter, img2xUrl };
or
export default {
capFirstLetter(val) {
return val.charAt(0).toUpperCase() + val.slice(1);
},
img2xUrl(val) {
return `${val.replace(/(\.[\w\d_-]+)$/i, '#2x$1')} 2x`;
},
};
You should then be able to use them anywhere in your components using:
this.$helpers.capitalizeFirstLetter()
or anywhere in your application using:
Vue.helpers.capitalizeFirstLetter()
You can learn more about this in the documentation: https://v2.vuejs.org/v2/guide/plugins.html
Create a new mixin:
"src/mixins/generalMixin.js"
Vue.mixin({
methods: {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
})
Then import it into your main.js like:
import '#/mixins/generalMixin'
From now on you will be able to use the function like this.capitalizeFirstLetter(str) within your component script or without this in a template. i.e.:
<template>
<div>{{ capitalizeFirstLetter('hello') }}</div>
</template>
You have to use this because you mixed a method into the main Vue instance. If there are ways of removing this it will probably involve something unconventional, this at least is a documented way of sharing functions which will be easy to understand for any future Vue devs to your project.
Using Webpack v4
Create a separate file for readability (just dropped mine in plugins folder).
Reproduced from #CodinCat and #digout responses.
//resources/js/plugins/mixin.js
import Vue from 'vue';
Vue.mixin({
methods: {
capitalizeFirstLetter: str => str.charAt(0).toUpperCase() + str.slice(1),
sampleFunction() {
alert('Global Functions');
},
}
});
Then, import in your main.js or app.js file.
//app.js
import mixin from './plugins/mixin';
USAGE:
Call this.sampleFunction() or this.capitalizeFirstLetter().
Use a global filter if it only concerns how data is formatted when rendered. This is the first example in the docs:
{{ message | capitalize }}
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
Great question. In my research I found vue-inject can handle this in the best way. I have many function libraries (services) kept separate from standard vue component logic handling methods. My choice is to have component methods just be delegators that call the service functions.
https://github.com/jackmellis/vue-inject
Import it in the main.js file just like 'store' and you can access it in all the components.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
render: h => h(App)
})