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.
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 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)
})
})
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'
});
I am working with Vue, by means of Quasar, with the pages being rendered via SSR. This works well enough, but I have a component that doesn't seem to behaving properly.
The issue is that the content is rendered correctly on the server side (verified by checking network log in Chrome), with the axios call loading in the data into an element using v-html, but when we get to the browser the state seems to be reset and server side rendered content gets lost, when using the 'elements' tab in the inspector.
Any ideas?
The Vue component is as follows:
<template>
<div class="dy-svg" v-html="svgData"></div>
</template>
<script>
/**
* This provides a way of loading an SVG and embedding it straight into
* the page, so that it can have css applied to it. Note, since we are
* using XHR to load the SVG, any non-local resource will have to deal
* with CORS.
*/
import axios from 'axios';
export default {
props: {
src: String,
prefetch: {
type: Boolean,
default: true
}
},
data() {
return {
svgData: undefined,
};
},
async serverPrefetch() {
if (this.prefetch) {
await this.loadImage();
}
},
async mounted() {
// if (!this.svgData) {
// await this.loadImage();
// }
},
methods: {
async loadImage() {
try {
let url = this.src;
if (url && url.startsWith('/')) {
url = this.$appConfig.baseUrl + url;
}
const response = await axios.get(url);
let data = response.data;
const idx = data.indexOf('<svg');
if (idx > -1) {
data = data.substring(idx, data.length);
}
this.svgData = data;
} catch (error) {
console.error(error);
}
}
}
};
</script>
Note, I did try add the v-once attribute to the div, but it seems to have no impact.
Environment:
Quasar 1.1.0
#quasar/cli 1.0.0
#quasar/app 1.0.6
NodeJS 10.15.3
Vue 2.6.10 (dependency via Quasar)
The fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, you should pre-fetch and fill data into the store while rendering. For this you can use Vuex.
Example Vuex store file:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
// import example from './module-example'
Vue.use(Vuex)
export default function ( /* { ssrContext } */ ) {
const Store = new Vuex.Store({
state: () => ({
entities: {}
}),
actions: {
async get({
commit
}) {
await axios.get('https://example.com/api/items')
.then((res) => {
if (res.status === 200) {
commit('set', res.data.data)
}
})
}
},
mutations: {
set(state, entities) {
state.entities = entities
},
},
modules: {},
// enable strict mode (adds overhead!)
// for dev mode only
strict: process.env.DEV
})
return Store
}
Example Vue page script:
export default {
name: 'PageIndex',
computed: {
// display the item from store state.
entities: {
get() {
return this.$store.state.entities
}
}
},
serverPrefetch() {
return this.fetchItem()
},
mounted() {
if (!this.entities) {
this.fetchItem()
}
},
methods: {
fetchItem() {
return this.$store.dispatch('get')
}
}
}
This should solve the issue you're facing.