Nuxt: Create a plugin that automatically adds a computed to component - vue.js

I would like to create a Nuxt plugin that automatically adds a computed to components that have a certain property (without using a mixin).
For example, any component that have a addComputedHere property:
export default {
data() {
return {}
},
computed: {
myComputed: () => 'foo'
},
addComputedHere: true
}
would turn into:
export default {
data() {
return {}
},
computed: {
myComputed: () => 'foo',
injectedComputed: () => 'bar' // Injected
},
addComputedHere: true
}
So far, I'm not sure what's the best solution among using a Nuxt plugin/module/middleware or simply a Vue Plugin (if it's feasible).
How would you do it?

If anybody is in the same case, I found a solution by creating a Vue plugin that applies a mixin to customize the component in beforeCreate:
import Vue from 'vue';
const plugin = {
install(Vue, options) {
Vue.mixin({
beforeCreate() {
if (this.$options.addComputedHere) {
this.$options.computed['injectedComputed'] = () => 'bar';
}
}
})
}
};
Vue.use(plugin);

Related

Vue3 reactive components on globalProperties

In vuejs 2 it's possible to assign components to global variables on the main app instance like this...
const app = new Vue({});
Vue.use({
install(Vue) {
Vue.prototype.$counter = new Vue({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ },
}
});
}
})
app.$mount('#app');
But when I convert that to vue3 I can't access any of the properties or methods...
const app = Vue.createApp({});
app.use({
install(app) {
app.config.globalProperties.$counter = Vue.createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
});
}
})
app.mount('#app');
Here is an example for vue2... https://jsfiddle.net/Lg49anzh/
And here is the vue3 version... https://jsfiddle.net/Lathvj29/
So I'm wondering if and how this is still possible in vue3 or do i need to refactor all my plugins?
I tried to keep the example as simple as possible to illustrate the problem but if you need more information just let me know.
Vue.createApp() creates an application instance, which is separate from the root component of the application.
A quick fix is to mount the application instance to get the root component:
import { createApp } from 'vue';
app.config.globalProperties.$counter = createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
}).mount(document.createElement('div')); ๐Ÿ‘ˆ
demo 1
However, a more idiomatic and simpler solution is to use a ref:
import { ref } from 'vue';
const counter = ref(1);
app.config.globalProperties.$counter = {
value: counter,
increment() { counter.value++ }
};
demo 2
Not an exact answer to the question but related. Here is a simple way of sharing global vars between components.
In my main app file I added the variable $navigationProps to global scrope:
let app=createApp(App)
app.config.globalProperties.$navigationProps = {mobileMenuClosed: false, closeIconHidden:false };
app.use(router)
app.mount('#app')
Then in any component where I needed that $navigationProps to work with 2 way binding:
<script>
import { defineComponent, getCurrentInstance } from "vue";
export default defineComponent({
data: () => ({
navigationProps:
getCurrentInstance().appContext.config.globalProperties.$navigationProps,
}),
methods: {
toggleMobileMenu(event) {
this.navigationProps.mobileMenuClosed =
!this.navigationProps.mobileMenuClosed;
},
hideMobileMenu(event) {
this.navigationProps.mobileMenuClosed = true;
},
},
Worked like a charm for me.
The above technique worked for me to make global components (with only one instance in the root component). For example, components like Loaders or Alerts are good examples.
Loader.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$loader = this;
}
},
...
AlertMessage.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$alert = this;
}
},
...
So, in the root component of your app, you have to instance your global components, as shown:
App.vue
<template>
<v-app id="allPageView">
<router-view name="allPageView" v-slot="{Component}">
<transition :name="$router.currentRoute.name">
<component :is="Component"/>
</transition>
</router-view>
<alert-message/> //here
<loader/> //here
</v-app>
</template>
<script lang="ts">
import AlertMessage from './components/Utilities/Alerts/AlertMessage.vue';
import Loader from './components/Utilities/Loaders/Loader.vue';
export default {
name: 'App',
components: { AlertMessage, Loader }
};
</script>
Finally, in this way you can your component in whatever other components, for example:
Login.vue
...
async login() {
if (await this.isFormValid(this.$refs.loginObserver as FormContext)) {
this.$loader.activate('Logging in. . .');
Meteor.loginWithPassword(this.user.userOrEmail, this.user.password, (err: Meteor.Error | any) => {
this.$loader.deactivate();
if (err) {
console.error('Error in login: ', err);
if (err.error === '403') {
this.$alert.showAlertFull('mdi-close-circle', 'warning', err.reason,
'', 5000, 'center', 'bottom');
} else {
this.$alert.showAlertFull('mdi-close-circle', 'error', 'Incorrect credentials');
}
this.authError(err.error);
this.error = true;
} else {
this.successLogin();
}
});
...
In this way, you can avoid importing those components in every component.

Getting access to varaibles when testing Vue with Jest

I am using the structure below in my Vue.js web application. I am now trying to implement testing to it. But when trying to test the exampleOfFunction it says that this.exampleOfData2 is undefined.
<template>
*Some HTML*
</template>
<script>
*Some Imports*
export default {
data() {
return {
exampleOfData1: [],
exampleOfData2: 100
},
methods: {
exampleOfFunction:function(){
if(this.exampleOfData2 === 100)
{
return false;
}
return true;
},
created() {
},
mounted() {
}
}
</script>
In my testfile I then try to access the code above and I succeed with console.log(FileToTest.data()); I can see the values of data and I can access the function with FileToTest.methods.exampleOfFunction(); but when I call the function it says that this.exampleOfData2 is undefined.
It looks like you're using the component options definition instead of the component instance in your tests.
You should be creating a wrapper by mounting the component, and then you could access the component method via wrapper.vm:
import { shallowMount } from '#vue/test-utils'
import FileToTest from '#/components/FileToTest.vue'
describe('FileToTest', () => {
it('exampleOfFunction returns false by default', () => {
const wrapper = shallowMount(FileToTest)
expect(wrapper.vm.exampleOfFunction()).toBe(false)
})
it('exampleOfFunction returns true when data is not 100', () => {
const wrapper = shallowMount(FileToTest)
wrapper.setData({ exampleOfData2: 0 })
expect(wrapper.vm.exampleOfFunction()).toBe(true)
})
})

How to call method in setup of vuejs3 app?

In vuejs3 app I retrieve data from db with axios in method, like :
<script>
import appMixin from '#/appMixin'
import app from './../../App.vue' // eslint-disable-line
import axios from 'axios'
const emitter = mitt()
export default {
name: 'adminCategoriesList',
mixins: [appMixin],
data: function () {
return {
categoriesPerPage: 20,
currentPage: 1,
categoriesTotalCount: 0,
categories: []
}
},
components: {
},
setup () {
const adminCategoriesListInit = async () => {
this.loadCategories() // ERROR HERE
}
function onSubmit (credentials) {
alert(JSON.stringify(credentials, null, 2))
console.log('this::')
console.log(this)
console.log('app::')
}
onMounted(adminCategoriesListInit)
return {
// schema,
onSubmit
}
}, // setup
methods: {
loadCategories() {
...
}
and I got error in browser's console :
Cannot read property 'loadCategories' of undefined
If to remove โ€œthis.โ€ in loadCategories call
I got error :
'loadCategories' is not defined
I need to make loadCategories as method, as I need to cal;l it from different places.
Which way is correct ?
Thanks!
You could use composition and options api in the same component but for different properties and methods, in your case the data properties could be defined inside setup hook using ref or reactive, the methods could be defined as plain js functions :
import {ref} from 'vue'
export default {
name: 'adminCategoriesList',
mixins: [appMixin],
components: {
},
setup () {
const categoriesPerPage= ref(20);
const currentPage=ref(1);
const categoriesTotalCount=ref(0),
const categories=ref[])
const adminCategoriesListInit = async () => {
loadCategories()
}
function onSubmit (credentials) {
alert(JSON.stringify(credentials, null, 2))
}
functions loadCategories(){
...
}
onMounted(adminCategoriesListInit)
return {
// schema,
onSubmit,
categoriesPerPage,
currentPage,
categoriesTotalCount,
categories
}
},
the properties defined by ref could be used/mutated by property.value and used in template like {{property}}

vue.js Import and load component dynamically

I am trying to get the following code dynamically import a component from a folder. However, vue doesn't import anything or show an error. (It's as if it doesn't sense the computed field changing).
What am I doing wrong? (I have already gone through the forums and this seems rare)
<template>
<component v-bind:is="column"></component>
</template>
<script>
export default {
name: "Column",
computed: {
column() {
return () => import(`../columns/${this.$store.state.column}.vue`);
},
}
};
</script>
First you need to import/register all components, you can either do this on component level like below or globally.
export default {
components: {
ColumnA: () => import('../columns/ColumnA'),
ColumnB: () => import('../columns/ColumnB'),
ColumnC: () => import('../columns/ColumnC'),
ColumnD: () => import('../columns/ColumnD'),
}
}
Next you have to make sure you can map your state to column-a to match the component name. if that's the case you can just use:
computed: {
column() {
return this.$store.state.column;
}
}
If not you would have to create a map:
computed: {
column() {
const mappedComponents = {
myStateKeyForColumnA: 'column-a',
myStateKeyForColumnB: 'column-b',
myStateKeyForColumnC: 'column-c',
myStateKeyForColumnD: 'column-d',
}
return mappedComponents[this.$store.state.column];
}
}
Edit
To register the components globally one can use require.context.
in main.js
const context = require.context('./path/to/columns', true, /\.vue$/)
for (const key of context.keys()) {
// key gives us the file name, ie. ./ColumnA.vue
// the code below, to register the component name is based on the above patterh
// likely you will have to modify this
Vue.component(key.slice(2).split('.')[0], () => context(key))
}

Vue-router: Using component method within the router

My first Vue project and I want to run a loading effect on every router call.
I made a Loading component:
<template>
<b-loading :is-full-page="isFullPage" :active.sync="isLoading" :can-cancel="true"></b-loading>
</template>
<script>
export default {
data() {
return {
isLoading: false,
isFullPage: true
}
},
methods: {
openLoading() {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
}, 10 * 1000)
}
}
}
</script>
And I wanted to place inside the router like this:
router.beforeEach((to, from, next) => {
if (to.name) {
Loading.openLoading()
}
next()
}
But I got this error:
TypeError: "_components_includes_Loading__WEBPACK_IMPORTED_MODULE_9__.default.openLoading is not a function"
What should I do?
Vuex is a good point. But for simplicity you can watch $route in your component, and show your loader when the $route changed, like this:
...
watch: {
'$route'() {
this.openLoading()
},
},
...
I think it's fast and short solution.
I don't think you can access a component method inside a navigation guard (beforeEach) i would suggest using Vuex which is a vue plugin for data management and then making isLoading a global variable so before each route navigation you would do the same ... here is how you can do it :
Of course you need to install Vuex first with npm i vuex ... after that :
on your main file where you are initializing your Vue instance :
import Vue from 'vue'
import Vuex from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isLoading: false,
},
mutations: {
openLoading(state) {
state.isLoading = true
setTimeout(() => {
state.isLoading = false
}, 10000)
},
},
})
// if your router is on a separated file just export the store and import it there
const router = new VueRouter({
routes: [
{
// ...
},
],
})
router.beforeEach((to, from, next) => {
if (to.name) {
store.commit('openLoading')
}
next()
})
new Vue({
/// ....
router,
store,
})
In your component:
<b-loading :is-full-page="isFullPage" :active.sync="$store.state.isLoading" :can-cancel="true"></b-loading>