I am a novice in Vue.js and I am trying to create my first plugin, something very simple, to be able to call it from my application as follows:
app.js
Vue.use(Pluginify, { option1: 1, option2: 2 });
The problem I am having is the following, I have in my index.js file of my plugin, a variable called configuration, this variable is the second argument that I passed to the Vue.use function, the problem is that I need to pass that variable to a custom component I'm creating, but can't. Please could you help me, this is the structure I have so far:
index.js
import Vue from 'vue';
import Plugin from './Plugin';
import { isObject } from 'lodash';
export const instance = new Vue({
name: 'Plugin',
});
const Pluginify = {
install(Vue, configuration = {}) { // This variable here, I need to pass it to the ```Plugin.vue``` component
Vue.component('plugin', Plugin);
const pluginify = params => {
if (isObject(params)) {
instance.$emit('create', params);
}
};
Vue.pluginify = pluginify;
Vue.prototype.$pluginify = pluginify;
}
};
export default Pluginify;
I have a component called Plugin that is empty, but I need it to contain the configuration object in order to use its values ββin the future.
Plugin.vue
<template>
</template>
<script>
import { instance } from './index';
export default {
name: 'Plugin',
mounted() {
instance.$on('create', this.create);
},
methods: {
create(params) {
}
}
};
</script>
<style scoped>
</style>
Thank you very much in advance
Ok, I have achieved it as follows:
index.js
const Pluginify = {
install(Vue, configuration = {}) {
/**
* Default plugin settings
*
* #type {Object}
*/
this.default = configuration;
....
So in my Plugin.vue component:
import * as plugin from './index';
So I can call in any method of my component the configuration parameters as follows:
...
mounted() {
console.log(plugin.default.option1);
},
...
I hope I can help anyone who gets here with a similar question.
It seems like this listener is added after the create event already fired
mounted() {
instance.$on('create', this.create);
},
You could use a global variable in your plugin like this ...
Vue.prototype.$pluginifyConfig = configuration;
and then you can call it with this.$pluginifyConfig in the plugin, or anywhere else.
But that pollutes the global scope
Maybe this could help:
const Plugin = {
template: '<div>Plugin</div>',
data() {
return {
a: 'a data'
};
},
mounted() {
console.log(this.a);
console.log(this.message);
}
};
const Pluginify = {
install(Vue, options) {
Vue.component('plugin', {
extends: Plugin,
data() {
return {...options}
}
});
}
};
Vue.use(Pluginify, {message: 'hello, world!'});
new Vue({
el: '#app'
});
Related
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);
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.
I'm new to Cypress component testing, but I was able to set it up easily for my Vue project. I'm currently investigating if we should replace Jest with Cypress to test our Vue components and I love it so far, there is only one major feature I'm still missing: mocking modules. I first tried with cy.stub(), but it simply didn't work which could make sense since I'm not trying to mock the actual module in Node.js, but the module imported within Webpack.
To solve the issue, I tried to use rewiremock which is able to mock Webpack modules, but I'm getting an error when running the test:
I forked the examples from Cypress and set up Rewiremock in this commit. Not sure what I'm doing wrong to be honest.
I really need to find a way to solve it otherwise, we would simply stop considering Cypress and just stick to Jest. If using Rewiremock is not the way, how am I suppose to achieve this? Any help would be greatly appreciated.
If you are able to adjust the Vue component to make it more testable, the function can be mocked as a component property.
Webpack
When vue-loader processes HelloWorld.vue, it evaluates getColorOfFruits() and sets the data property, so to mock the function here, you need a webpack re-writer like rewiremock.
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
colorOfFruits: getColorOfFruits(), // during compile time
};
},
...
Vue created hook
If you initiialize colorOfFruits in the created() hook, you can stub the getColorOfFruits function after import but prior to mounting.
HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>{{ colorOfFruits.apple }}</h2>
<p>
...
</template>
<script lang="ts">
import { getColorOfFruits } from "#/helpers/fruit.js";
export default {
name: "HelloWorld",
getColorOfFruits, // add this function to the component for mocking
props: {
msg: String,
},
data() {
return {
colorOfFruits: {} // initialized empty here
};
},
created() {
this.colorOfFruits = this.$options.colorOfFruits; // reference function saved above
}
});
</script>
HelloWorld.spec.js
import { mount } from "#cypress/vue";
import HelloWorld from "./HelloWorld.vue";
it("mocks an apple", () => {
const getMockFruits = () => {
return {
apple: "green",
orange: "purple",
}
}
HelloWorld.getColorOfFruits = getMockFruits;
mount(HelloWorld, { // created() hook called as part of mount()
propsData: {
msg: "Hello Cypress!",
},
});
cy.get("h1").contains("Hello Cypress!");
cy.get("h2").contains('green')
});
We solved this by using webpack aliases in the Cypress webpack config to intercept the node_module dependency.
So something like...
// cypress/plugins/index.js
const path = require("path");
const { startDevServer } = require('#cypress/webpack-dev-server');
// your project's base webpack config
const webpackConfig = require('#vue/cli-service/webpack.config');
module.exports = (on, config) => {
on("dev-server:start", (options) => {
webpackConfig.resolve.alias["your-module"] = path.resolve(__dirname, "path/to/your-module-mock.js");
return startDevServer({
options,
webpackConfig,
})
});
}
// path/to/your-module-mock.js
let yourMock;
export function setupMockModule(...) {
yourMock = {
...
};
}
export default yourMock;
// your-test.spec.js
import { setupMock } from ".../your-module-mock.js"
describe("...", () => {
before(() => {
setupMock(...);
});
it("...", () => {
...
});
});
This is my Vue Instance:
const app = new Vue({
el: '#app',
data() {
return {
loading: false
}
},
components: {
App,
Loading
},
router,store,
})
How can I change the loading variable from multiple layer down? It's not just simple parent/child change. It can be greatgrandparent/child change too.
There are several options, but the best way to answer the questions is to register as a global plug-in.
See the following solutions:
Create loading component and register it as a Vue component.
// loadingDialog.vue
<template>
<!-- ... -->
</template>
<script>
import { Loading } from 'path' // Insert the `loading` declared in number 2.
export default {
beforeMount () {
Loading.event.$on('show', () => {
this.show()
})
},
methods: {
show () {}
}
}
</script>
Creating a global plug-in
const loading = {
install (Vue, opt = {}) {
let constructor = Vue.extend(LOADING_COMPONENT)
let instance = void 0
if (this.installed) {
return
}
this.installed = true
Vue.prototype.$loadingDialog = {
show () {
if (instance) {
instance.show() // function of loading component
return
}
instance = new constructor({
el: document.createElement('div')
})
document.body.appendChild(instance.$el)
instance.show() // function of loading component
}
}
}
}
All components are accessible through the prototype.
this.$loadingDialog.show() // or hide()
Note, it is recommended that you control the api communication using the axios in the request or response interceptorof the axios at once.
How am i able to mock $parent for my specs? When using shallowMount with my component i always get clientWidth/clientHeight of undefined. I already tried mocking $parent as an object with an $el as a key and two more nested keys for clientWidth and clientHeight, but that's not working as expected. I cannot figure out the right usage of parentComponent.
I've got a single file component as seen below:
<template>
<img :src="doSomething">
</template>
<script>
export default {
name: "Foobar",
data() {
return {
size: null
};
},
computed: {
doSomething() {
# here is some string concatenation etc.
# but not necessary for example
return this.size;
}
},
created() {
let parent = this.$parent.$el;
this.size = `size=${parent.clientWidth}x${parent.clientHeight}`;
}
};
</script>
creating the vue app looks like this:
import Vue from "vue";
import Foobar from "./Foobar";
const vueEl = "[data-vue-app='foobar']";
if (document.querySelector(vueEl)) {
new Vue({
el: vueEl,
components: {
"foo-bar": Foobar
}
});
}
and the combination of using slim with my component looks like this:
div data-vue-app="foobar"
foo-bar
This is my test setup:
import { shallowMount } from "#vue/test-utils";
import Foobar from "#/store/Foobar";
describe("Foobar.vue", () => {
let component;
beforeEach(() => {
component = shallowMount(Foobar, {});
});
The parentComponent mounting option expects a component object:
import Parent from "../Parent.vue";
...
beforeEach(() => {
component = shallowMount(Foobar, {
parentComponent: Parent
});
});
Also, try pinning the version of vue-test-utils to "^1.0.0-beta.28". This should allow your tests to complete, but the clientWidth/Height will still be 0x0
ref: https://vue-test-utils.vuejs.org/api/options.html#parentcomponent