vue eventbus - $on wont trigger the bus - vue.js

I have looked at tutorials and read the papers but I don’t get it why my setup with eventbus does not work.
In main.js
I create a new instance of Vue
/* create a eventbus*/
export const Bus = new Vue();
In page1
import { Bus } from "../main";
I then have a click event that’s triggers a method
methods: {
moveData(inValue) {
let valueToSend = inValue;
console.log("valueToSend");
console.log(valueToSend);
Bus.$emit("emitAlbumTitle", valueToSend);
},
},
And console.log tells there nothing wrong with the method moveData().
In page 2.
I try to listen to the busemit.
import { Bus } from "../main";
data() {
return {
id: this.$route.params.idAlbum,
photoData: [],
albumTitle: "",
};
},
In tried in created(), I have some other things going on there as you see, like an api-call but that should not affect this I think.
async created() {
try {
this.photoData = await CallApi.getPosts(url + this.id);
this.number = this.photoData.length;
Bus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
} catch (err) {
this.error = err.message;
}
},
But nothing in the console.logs in the Bus.$on starts, so that eventbus never starts?
I also have tried in mounted() hook
mounted() {
Bus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
},
But same result.
What am I missing here?

The problem is your Receiver component is not created until you click the link, at which point the event has already been emitted from Sender.
One solution is to delay the event emitted until the next macro tick (using setTimeout without a delay), as the Receiver component would be created in the current macro tick:
export default {
methods: {
async emitValue() {
// wait til next macro tick
await new Promise(r => setTimeout(r));
EventBus.$emit("string-send", this.sendString);
},
}
}
demo

Try same as this works for me. Data will show in console on button click.
Here is the main.js file.
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify';
Vue.config.productionTip = false
export const eventBus = new Vue();
new Vue({
vuetify,
render: h => h(App)
}).$mount('#app')
It is App.vue file.
<template>
<v-app>
<div id="app">
{{albumTitle}}
<br>
<button class="primary" #click="dataSend">send</button>
</div>
</v-app>
</template>
<script>
import {eventBus} from '#/main'
export default {
name: "App",
data: () => ({
albumTitle: null
}),
created(){
eventBus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
},
methods: {
dataSend(){
eventBus.$emit("emitAlbumTitle", "some data")
}
},
};
</script>

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.

How to emit events from component to main instance usint $emit or vuex

I want to use my main vuejs instance to manage sockets.io connection and events. I have this code that works, but I have some problems to pass events from component to parent instance. The code is inside a chrome extension that use vuex, but I'm not familiar with vuex at the moment. How I can pass events between my main instance and child component? Someone has suggested me to use vuex, but it's divided in three files and I'm not able to understand for now how to obtain what I want.
<script>
// child component
export default {
data() {
return {
isRegistered: false,
isConnected: false
}
},
mounted() {
this.$on('connected', function(event) {
console.log(event)
})
},
methods: {
initRoom() {
console.log('clicked!')
this.$emit('openConnection')
}
}
}
</script>
// main instance
import Vue from 'vue'
import App from './App'
import store from '../store'
import router from './router'
import VueSocketIOExt from 'vue-socket.io-extended';
import io from 'socket.io-client';
const socket = io('https://lost-conn.herokuapp.com', {
autoConnect: false
});
Vue.use(VueSocketIOExt, socket, { store });
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
render: h => h(App),
mounted() {
this.$on('openConnection', function() {
socket.open()
alert('k')
})
},
data: {
isRegistered: false,
isConnected: false,
message: ''
},
sockets: {
connect() {
console.log('socket connected')
this.$emit('connected', 'socket connected')
},
},
methods: {}
})
So, you can try vuex but it seems kind of heavy if all you want is a basic event listener. One option might be to go with the eventBus route and set up an emitter and a listener event. in main.js you can add
export const eventBus = new Vue()
Then in your code you could swap this.$emit('connected', 'socket connected')
with eventBus.$emit('connected', true_or_any_other_value_here)
Then in your component that you're listening for the event. Import eventBus from main.js and add:
data: ( => ({ bus: eventBus }),
created() {
this.bus.$on('connected', ($event) => myCallbackFunction($event) )
},
I think this should do the trick, I haven't yet tried the callback function and passing data as I usually do my check on the front end but if there is data object you need to store please specify and I might be able to help you through using vuex.

unable to retrieve mutated data from getter

I'm trying to render a d3 graph using stored data in vuex. but I'm not getting data in renderGraph() function.
how to get data in renderGraph()?
Following is store methods.
store/index.js
import Vue from "vue";
import Vuex from "vuex";
import * as d3 from "d3";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
subscribers: []
},
getters: {
getterSubscribers: state => {
return state.subscribers;
}
},
mutations: {
mutationSubscribersData: (state, payload) => {
state.subscribers = payload;
}
},
actions: {
actionSubscribersData: async ({ commit }) => {
let subsData = await d3.json("./data/subscribers.json"); // some json fetching
commit("mutationSubscribersData", subsData);
}
}
});
Below is parent component
Home.vue
<template>
<div>
<MyGraph /> // child component rendering
</div>
</template>
<script>
import MyGraph from "./MyGraph.vue";
export default {
components: {
MyGraph
},
};
</script>
Below is child component.
MyGraph.vue
<template>
<div>
<svg width="500" height="400" />
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
computed: {
...mapGetters(["getterSubscribers"])
},
methods: {
...mapActions(["actionSubscribersData"]),
renderGraph(data) {
console.log(data); // DATA NOT COMING HERE
// MyGraph TO BE RENDERED WITH THIS DATA
}
},
mounted() {
this.actionSubscribersData();
this.renderGraph(this.getterSubscribers);
}
};
</script>
I have tried mounted, created lifecycle hooks. but did not find data coming.
There is race condition. actionSubscribersData is async and returns a promise. It should be waited for until data becomes available:
async mounted() {
await this.actionSubscribersData();
this.renderGraph(this.getterSubscribers);
}
There must be delay for the actionSubscribersData action to set value to store. Better you make the action async or watch the getter. Watching the getter value can be done as follows
watch:{
getterSubscribers:{ // watch the data to set
deep:true,
handler:function(value){
if(value){ // once the data is set trigger the function
this.renderGraph(value);
}
}
}
}

Inform App.vue a Service-Worker Event is Being Called

I have a project created using Vue CLI 3 with Vue's PWA plugin included. I would like to display a banner prompting the user to click an in-app “Refresh” link as described here in the 'Approach #3' section.
But in my Vue.js app, because the service-worker code is executed in main.js, and my snackbar banner is built into my App.vue component, I'm not sure how to trigger my showRefreshUI() method once the service-worker updated() event has been called.
main.js (applicable portion)
import Vue from 'vue';
import App from './App';
import './registerServiceWorker';
new Vue({
router,
render: h => h(App),
}).$mount('#app');
register-service-worker (applicable portion)
import { register } from 'register-service-worker';
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
updated (registration) {
console.log('New content is available; please refresh.');
// I'd like to call App.vue's showRefreshUI() method from here.
},
});
}
App.vue (applicable portion)
<script>
export default {
name: 'App',
mounted () {
// Alternatively, I'd like to call this.showRefreshUI() from here
// when the service worker's updated() method is called.
},
methods: {
showRefreshUI () {
// My code to show the refresh UI banner/snackbar goes here.
},
},
};
</script>
If I can't call the showRefreshUI() method from main.js, how might I pass something from the updated() event to App.vue's mounted() lifecycle hook to accomplish the same basic thing?
The final working solution for me was to leave main.js untouched, and instead:
register-service-worker (applicable portion)
import { register } from 'register-service-worker';
const UpdatedEvent = new CustomEvent('swUpdated', { detail: null });
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
updated (registration) {
console.log('New content is available; please refresh.');
UpdatedEvent.detail = registration;
document.dispatchEvent(UpdatedEvent);
},
});
}
App.vue (applicable portion)
<script>
export default {
name: 'App',
data () {
return {
registration: null,
};
},
mounted () {
document.addEventListener('swUpdated', this.showRefreshUI);
},
beforeDestroy () {
document.removeEventListener('swUpdated', this.showRefreshUI);
},
methods: {
showRefreshUI (e) {
this.registration = e.detail;
// My code to show the refresh UI banner/snackbar goes here.
},
},
};
</script>
I'm not sure but I think you could use a custom event for this purpose. Something like this might work for you ..
1) Create the custom event in your main.js ..
main.js
import Vue from 'vue';
import App from './App';
import './registerServiceWorker';
const updateEvent = new Event('SWUpdated');
new Vue({
router,
render: h => h(App),
}).$mount('#app');
2) Dispatch the custom event when the service worker is updated ..
register-service-worker
import { register } from 'register-service-worker';
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
updated (registration) {
console.log('New content is available; please refresh.');
document.dispatchEvent(updateEvent);
},
});
}
3) Attach an event listener to the document object in your mounted hook that listens for your custom event. Remove the event listener in the beforeDestroy hook ..
App.vue
<script>
export default {
name: 'App',
mounted () {
document.addEventListener('SWUpdated', this.showRefreshUI);
},
beforeDestroy () {
document.removeEventListener('SWUpdated', this.showRefreshUI);
},
methods: {
showRefreshUI () {
// My code to show the refresh UI banner/snackbar goes here.
},
},
};
</script>

How to pass property to component used in main.js from other components in vuejs

Basically I want to a loadingbar component globally (included in app template)
Here is my loadingbar component
<template>
<div class="loadingbar" v-if="isLoading">
Loading ...
</div>
</template>
<script>
export default {
name: 'loadingbar',
props: ['isLoading'],
data () {
return {
}
}
}
</script>
<style scoped>
</style>
and in main.js, I have included this component as
import LoadingBar from './components/LoadingBar.vue';
new Vue({
router,
data () {
return {
isLoading: true
};
},
methods: {
},
created: function () {
},
components: {
LoadingBar
},
template: `
<div id="app">
<LoadingBar :isLoading="isLoading"/>
<router-view></router-view>
</div>
`
}).$mount('#app');
My aim is to show loading component based upon the value of variable isLoading. The above code working fine. But I want to use set isLoading variable from other component (so that to decide whether to show loading component). Eg. In post components
<template>
<div class="post container">
</div>
</template>
<script>
export default {
name: 'post',
data () {
return {
posts: []
}
},
methods: {
fetchPosts: function() {
// to show loading bar
this.isLoading = true;
this.$http.get(APIURL+'listpost')
.then(function(response) {
// to hide loading bar
this.isLoading = false;
console.log("content loaded");
});
}
},
created: function() {
this.fetchPosts();
}
}
</script>
<style scoped>
</style>
Of coarse we can't access isLoading directly from main.js so i decided to use Mixin so i put following code in main.js
Vue.mixin({
data: function () {
return {
isLoading: false
};
}
});
This however allow me to access isLoading from any other component but I can't modify this variable. Can any help me to achieve this?
Note: I know i can achieve this by including loadingbar in individual component (I tried that and it was working fine, But i do not want to do that as loadingbar is needed in every component so i was including in main template/component)
You could use Vuex like so:
// main.js
import Vuex from 'vuex'
let store = new Vuex.Store({
state: {
isLoading: false,
},
mutations: {
SET_IS_LOADING(state, value) {
state.isLoading = value;
}
},
getters: {
isLoading(state) {
return state.isLoading;
}
}
})
import LoadingBar from './components/LoadingBar.vue';
new Vue({
router,
store, // notice you need to add the `store` var here
components: {
LoadingBar
},
template: `
<div id="app">
<LoadingBar :isLoading="$store.getters.isLoading"/>
<router-view></router-view>
</div>
`
}).$mount('#app');
// script of any child component
methods: {
fetchPosts: function() {
// to show loading bar
this.$store.commit('SET_IS_LOADING', true);
this.$http.get(APIURL+'listpost')
.then(function(response) {
// to hide loading bar
this.$store.commit('SET_IS_LOADING', false);
console.log("content loaded");
});
}
},