Why cant' I trigger custom events in vue.js? - javascript

I have a local database that will be updated with pusher. This database is stored in JSON and the component will load and filter out what is not needed, making it impossible to add a watcher to the raw data.
My idea (please advice me if there are better solution) was to add a listener for a custom event, then trigger this event when the DB is updated. The only event I'm able to trigger is 'CLICK', with a simple $('.vue-template').trigger('click');
This is paired with a simple #click="doSomething" on the element I chose as my target.
This is not ideal because I don't want to fetch the data on any click, but only when the DB is updated, so I've tried #customevent="doSomething" but it doesn't work
Any help?
EDIT: More code
<template>
<div class="listen listen-database-teachers"
#dbupdate="testAlert">
<span v-for="teacher in teachers"
class="pointer btn btn-sm btn-default destroy-link"
:data-href="computedHref(teacher)"
:data-person="personIdentifier(teacher.person)"
:data-personid="teacher.person_id">
{{ personIdentifier(teacher.person) }} <span class="glyphicon glyphicon-remove"></span>
</span>
</div>
</template>
<script>
export default {
props: ['circleId'],
data () {
return {
loading: false,
teachers: null,
error: null
}
},
created () {
// fetch the data when the view is created and the data is
// already being observed
this.fetchData();
},
methods: {
fetchData() {
this.error = this.teachers = this.categories = null;
this.loading = true;
var teachers = $(window).localDatabase('teachers', $(window).planner('currentSchoolId'));
var searchCircle = this.circleId,
filtered_teachers = [];
$.each(teachers, function(index, teacher){
if(teacher.membership_id == searchCircle)
filtered_teachers.push(teacher);
});
this.teachers = filtered_teachers.sort(function(a, b) {
return personIdentifier(a.person) > personIdentifier(b.person);
});
this.loading = false;
},
computedClass(teacher){
return 'pointer btn btn-sm btn-default draggable membership-draggable-'+teacher.membership_id+' destroy-link'
},
computedHref(teacher){
return window.links.destroy_membership
+ '?association-id='+teacher.id
+ '&circle-id='+teacher.circle_id;
},
testAlert: function(){
return alert('success');
}
}
}
</script>
EDIT 2: attempt to use BUS
Remember that I use Laravel Mix.
app.js
window.vue_bus = new Vue();
master.js
function handleDatabaseUpdate(){
window.vue_bus.$emit('refresh-data');
}
component
created () {
// fetch the data when the view is created and the data is
// already being observed
this.fetchData();
window.vue_bus.$on('refresh-data', this.testAlert());
},
I've isolated the relevant bits of the code. This executes testAlert() when the component is mounted, but it returns an error cbs[i] undefined when I call handleDatabaseUpdate() from browser console (on Firefox).

This is a case where I might use an event bus.
A bus is created by simply creating an empty Vue.
const bus = new Vue()
In whatever function you have outside that is dealing with the database, you can emit an event using the bus.
function handleDatabaseUpdate(){
//do database things
bus.$emit("refresh-data")
}
Then in your component, add a handler.
created(){
bus.$on("refresh-data", this.fetchData)
}
Since it looks like you are using single file components, you will have to make the bus available to both the component and your database manager. You can do that two ways. Quick and dirty, expose the bus on the window.
window.bus = new Vue()
Second, you can create the bus in its own module and import the module into all the places you need to use it.
bus.js
import Vue from "vue"
const bus = new Vue()
export default bus;
And in the code that deals with the database and in your component, import the bus.
import bus from "/path/to/bus.js
That said, the reason you cannot listen to jQuery events in Vue is that jQuery uses it's own event system and Vue's listeners will never catch jQuery events. If you still wanted to use jQuery, you could emit a custom event in jQuery and listen to that custom event also using jQuery. You would just have to dispatch the event to fetchData.
created(){
$("thing you sent the event from").on("refresh-data", this.fetchData)
}
Finally, this is also a case where a state management system (like Veux) would shine. State management systems are intended for cases like this where state could be changing in many places and automatically reflecting those changes in the view. There is, of course, more work implementing them.

Related

Unable to listen to emitted events with Ionic 5

I created a simple ionic app that allows users to book services. The user would select a service category, choose a service(s) then navigate to a form to complete the booking.
I've setup an event bus using tiny emitter since the project uses vue 3 with the composition api. The data emits as expected however when navigating to the booking form the listener is not triggered.
The expected behaviour is to get the selected service(s) and send it along with the rest of the booking info to a REST api.
eventBus.js
import { TinyEmitter } from 'tiny-emitter';
const emitter = new TinyEmitter();
const eventBus = () => {
return { emitter };
};
export default eventBus;
Service.vue
// template
<ion-button routerDirection="forward" routerLink="/booking" #click="sendEvent">Book Now</ion-button>
// script
import eventBus from './eventBus';
export default {
...
setup() {
...
const sendEvent = () => {
eventBus().emitter.emit('selected-service', 100) // the real code emits an array
}
return { sendEvent }
}
}
Booking.vue - Nothing happens in the console log
<script>
...
onMounted(() => {
eventBus().emitter.on('selected-service', (payload) => {
console.log('listener', payload);
})
})
</script>
I know this works in a regular Vue 3 project but I'm not sure why it's not working with ionic.
Things I've tried
Using the native emitter via context as a setup param. https://v3.vuejs.org/guide/composition-api-setup.html#accessing-component-properties
Using the mitt package as described here: Vue 3 Event Bus with Composition API
Emitting the event when the user chooses a service rather than when they click "Book Now"
Calling the listener in setup() directly rather than onMounted.
UPDATE
I noticed the listener gets called when I navigate off the booking page then back to it. So if I go from service details -> booking -> back to service details -> booking it triggers the bus and the payload is captured.
This may be a framework level bug. I've spoken to the Ionic team via twitter and was advised to use query params instead so that's the route I took.
may be rxjs helps you
import { Subject } from 'rxjs';
private newProduct = new Subject<any>();
publishNewProduct() {
this.newProduct.next();
}
subscribeNewProduct(): Subject<any> {
return this.newProduct;
}
Just had the same problem, to make it worse, in my case the bug was intermittent and only present if the dev tools console was closed. Opening the dev tools or using alerts would result in the component being rendered in time to receive the event.. I almost lost my sanity over it.
In my case using a watcher on the a prop using immediate: true was the cleanest solution.
I think this bug is really nasty since global events support have been removed from Vue2 and the Vue3 upgrade docs explicitly suggest to use tiny emitter to achieve it.
This leads to weird behaviors and bugs that are almost impossible to trace back. For this reason, I think the global event pattern should be avoided as much as possible.
As a final note if it can help someone, this is how I ended up being able to use console logs to trace the problem back to the global event bus:
const logs = []
app.config.globalProperties.$log = function () { logs.push(arguments) }
window.viewlogs = () => {
for (let i = 0; i < logs.length; i++) {
console.log.apply(window, logs[i])
}
}
Once the bug occured I could open the dev tools and view logs using window.viewlogs()

How can I emit localstorage change with Vue?

I have some problems with my Vue app.
I'm trying to update user information that are stored in localStorage, and I'm updating it with websockets in App.vue in mounted function, like so:
window.Echo.channel("user." + this.userData.id).listen(".user-updated", (user) => {
localStorage.setItem('userData', JSON.stringify(user.user))
});
And so far, so good actually, localstorage is updating realtime, but the problem is, the user must refresh the page or change route so other information that is shown in another components could be updated.
Is there any way I can update all my components at once from App.vue?
You can emit events using event bus, or else use Vuex. For example:
window.Echo.channel("user." + this.userData.id).listen(".user-updated", (user) => {
localStorage.setItem('userData', JSON.stringify(user.user));
//emit event
EventBus.$emit('EVENT_USER_DATA_CHANGE', payLoad);
});
Then, on other components:
mounted () {
EventBus.$on(‘ EVENT_USER_DATA_CHANGE’, function (payLoad) {
...
});
}
You can use :key binding on your body or main container. (e.g. <main :key="pageTrigger">)
In your listen function update pageTrigger with new Date().getTime() so it will re-render the components inside the container.
Are you using vuex too? put the pageTrigger in some store so it will be accessible from everywhere in your app.

Event never gets emitted in VueJS within an axios call

I have a component which needs to initialize data at the beginning, it does some transformations to the data (based on some values which are saved in localstorage to mark those data as selected).
Using Vue 2.5.
// component.vue
import Vue from 'vue'
export default Vue.extend({
data() {
fetchedData: [],
},
mounted() {
this.getStuff()
window.Bus.$on('localStorageRefreshed', this.getStuff)
},
computed: {
selectedData() {
return this.fetchedData.filter(data => data.selected)
}
},
methods: {
getStuff() {
const doTransformations = function (res, existing) {
// blabla
}
axios.get('/endpoint/for/stuff/').then(result => {
doTransformations(result, this.fetchedData) // not exactly what happens, but I think this is unneeded to solve my problem. mostly there to illustrate how this all fits together.
window.Bus.$emit('stuffFetched', this.selectedData)
})
},
}
})
So window.Bus is a Vue instance which is just posing as a global event handler. Within my getStuff-method, the event never gets emitted, and I have no idea why. I've console logged everywhere in order to figure out if the bus is just not initialized (it is, because I have tons of components which works perfectly with it). The event is just never emitted. I've tried wrapping the emitting in $nextTick but that doesn't work either (I also tried doing this directly in the mounted-lifecycle method, because the computed property updates in Vue devtools like it should and contains all the right stuff).
I'm really at a loss of what to do here, everything seems to work like it should, but the event is not even registered in Vue devtools.
The reason I need to fire this event is in order to do some price calculations for another component which exists outside of the scope of this child and it's parent. Just emitting in the child scope (this.$emit('dataChanged')) doesn't emit an event either using this approach.
Anyone have an idea of what my ******* brain is doing to me here?
Did you try to use async await?
That i'll probably make the timeout job, something like:
async mounted() {
await this.getStuff()
window.Bus.$on('localStorageRefreshed', this.getStuff)
}
and then do that on your getStuff too:
async getStuff() {
const doTransformations = function (res, existing) {
// blabla
}
await axios.get('/endpoint/for/stuff/').then(result => {
doTransformations(result, this.fetchedData) // not exactly what happens,
but I think this is unneeded to solve my problem. mostly there to
illustrate how this all fits together.
window.Bus.$emit('stuffFetched', this.selectedData)
})
}

Vue.js: EventBus.$on received value is not propagating

I am fairly new to vue and trying to figure out the best way to structure my event bus. I have a main layout view (Main.vue) inside of which I have a router view that I am passing a child's emitted info to like so:
<template>
<layout>
<router-view :updatedlist="mainupdate" />
</layout>
</template>
import { EventBus } from './event-bus.js'
export default {
data () {
return {
mainupdate: []
}
},
mounted () {
EventBus.$on('listupdated', item => {
this.mainupdate = item
console.log(item)
})
}
}
The structure looks like: Main.vue contains Hello.vue which calls axios data that it feeds to child components Barchart.vue, Piechart.vue, and Datatable.vue
The axios call in Hello.vue populates a data property called list. I then check if updatedlist (passed as router prop from Datatable.vue to Main.vue when something changes) is empty, and if so set it to the value of list
I know that the event is being emitted and received by Main.vue because the console.log(item) shows the data. But my child components are not getting updated, even though they are using updatedlist as their data source. (If I reload the page, they will be updated btw, but why aren't they reactive?)
UPDATE: I thought I would cut out the top level parent Main.vue and just put my EventBus$on inside Hello.vue instead and then just change list when it was received. But this does two things, neither of which are great:
On every page refresh, my console logs add one more log to the list and output all of them. So for example if I have made 3 changes, there will be three logs, 4 there will be 4, etc. The list grows until I restart the app.
My child components are STILL not updated :(
UPDATE 2: UGH. I think I see why they aren't updating, the problem is with the reactivity in my chartsjs component, so I will have to resolve that there (Barchart.vue, Piechart.vue). A simple component I built myself to just read the total length DOES get updated, so that works. This still leaves the mystery of the massive number of duplicate console logs though, any ideas?
It sounds like you may have the answer to the original question? To answer your last update you likely have duplicate logs because you do not appear to be removing event handlers. When you add an event handler to a bus like this:
mounted () {
EventBus.$on('listupdated', item => {
this.mainupdate = item
console.log(item)
})
}
}
You need to remove it yourself. Otherwise you are just adding a new handler every time the component is mounted.
I suggest you move the actual handler into a method:
methods: {
onListUpdated(item){
this.mainupdate = item
console.log(item)
}
}
Move code to add the handler to created:
created() {
EventBus.$on('listupdated', this.onListUpdate)
}
}
And add a beforeDestroy handler:
beforeDestroy(){
EventBus.$off("listupdated", this.onListUpdate)
}
And do that in every component that you are adding event handlers to EventBus.
I' not aware if you require to stick to the current structure. For me the following works perfectly fine.
// whereever you bootstrap your vue.js
// e.g.
window.Vue = require('vue');
// add a central event bus
Vue.prototype.$bus = new Vue();
Now you can simply access the event bus in every component by using
this.$bus.$on( ... )
As said I'm not aware of your full event bus code but this should actually work fine

How to bubble events on a component subcomponent chain with Vue js 2?

I have my vue application using:
component-parent component that is made of component-child
inside component-parent I have buttons, when someone click a button I want to emit an event in order to be handled by vue and passed to another component
What I did so far:
var vm = new Vue({
el: '#app',
methods: {
itemSelectedListener: function(item){
console.log('itemSelectedListener', item);
}
}
});
Vue.component('component-child', {
template: ' <span v-on:click="chooseItem(pty )" >Button </span>',
methods: {
chooseItem: function(pty){
console.log(pty);
this.$emit('itemSelected', {
'priority' : pty
});
}
}
});
Vue.component('component-parent', {
template: '<component-child v-for="q in items" ></component-child>'
});
HTML:
<component-parent v-on:itemSelected="itemSelectedListener" ></component-parent>
It reaches my console.log(pty); line but it seems that this.$emit('itemSelected' wont get through:
console.log('itemSelectedListener', item); // this is not going to be called...
an hint?
should I bubble up the event from child->parent->Vue-instance? ( I also tried that but with no success)
There is one issue with your component-parent template as it tries to render multiple child components. Vue usually requires a single root div inside components therefore you need to wrap it in a div or other tag.
<div>
<component-child v-for="q in items"></component-child>
</div>
A second thing to point out is that you emit an event from a child component which is 2 levels down and you listen to it in the root.
Root //but you listen to the event up here 1 level above
Component 1 //you should listen to the event here
Component 2 //your try to emit it from here
You have 2 options here. Either emit from component-child listen to that event in component-parent then propagate that event upwards. Fiddle https://jsfiddle.net/bjqwh74t/29/
The second option would be to register a global so called bus which is an empty vue instance that you can use for such cases when you want communication between non child-parent components. Fiddle https://jsfiddle.net/bjqwh74t/30/
Usually between parent and child components you use the events directly by emitting from child and listening in parent with v-on:event-name="handler" but for cases where you have more levels between components you use the second approach.
Doc link for the first case: https://v2.vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events
Doc link for the second case: https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
PS: prefer using kebab-case for event names which means you write with - instead of capital letters. Writing with capital letters can result in weird situations where your event is not caught in the root.
For what it's worth you can use the browser's event API. It requires a little more scripting than Vue's built-in stuff, but it also gets you around these bubbling issues (and is about the same amount of code as creating a "bus", as in the accepted answer).
On child component:
this.$el.dispatchEvent(new CustomEvent('itemSelected', { detail: { 'priority' : pty }, bubbles: true, composed: true });
On parent component, in mounted lifecycle part:
mounted() {
this.$el.addEventListener('itemSelected', e => console.log('itemSelectedListener', e.detail));
}
It's a little bit late but here's how I did it:
component-child:
this.$root.$emit('foobar',{...});
component-parent:
this.$root.$on('foobar')
In your child component, simply use $emit to send an event to the $root like this:
v-on:click="$root.$emit('hamburger-click')"
Then, in your parent component (eg: "App"), setup the listener in the Vue mounted lifecycle hook like this:
export default {
<snip...>
mounted: function() {
this.$root.$on('hamburger-click', function() {
console.log(`Hamburger clicked!`);
});
}
}
I'm a little surprised nobody suggested using an event bus component. It is a fairly common pattern in heavily decoupled systems to have a shared event bus, and then use it to link multiple disconnected components together pub/sub style.
//eventbus.js
import Vue from 'vue'
export const EventBus = new Vue()
Once you have one, it's simple to publish events from any place
// component1.js
import { EventBus } from '#/services/eventbus'
...
EventBus.$emit('do-the-things')
And listen to them from somewhere else.
// component2.js
import { EventBus } from '#/services/eventbus'
...
EventBus.$on('do-the-things', this.doAllTheThings)
Note that the two components do not know anything about each other, and they don't really need to care how or why the event was raised.
There are some potentially undesirable side effects of this approach. Event names do need to be globally unique so that you don't confuse your app. You'll also potentially be channeling every single event through a single object unless you do something more sophisticated. You can do the cost / benefit analysis on your own app source to see if it's right for you.
Create a custome directive for bubbling and use it in the child component.
Sample directive:
// Add this to main.ts when initializing Vue
Vue.directive('bubble', {
bind(el, { arg }, {context, componentInstance}) {
if (!componentInstance || !context || !arg) {
return;
}
// bubble the event to the parent
componentInstance.$on(v, context.$emit.bind(context, arg));
}
});
Child component can use that directive to emit through the parent component (i switched to kabob case and shorthand for event).
<!-- template for component-parent -->
<component-child v-bubble:item-selected v-for="q in items"></component-child>
<!-- usage of component-parent -->
<component-parent #:item-selected="itemSelectedListener"></component-parent>
Including my full bind directive below. It allows bubbling multiple events.
<!--------------------
* A few examples
--------------------->
<!-- bubble single event -->
<child v-bubble:click/>
<child v-bubble:_.click/>
<child v-bubble="'click'"/>
<child v-bubble:any-costume-event-will-work/>
<!-- bubble: click, focus, blur -->
<child v-bubble:_.click.focus.blur/>
<child v-bubble="'click, focus, blur'"/>
<!-- prefixed bubbling: click, focus, blur as child-click, child-focus, child-blur -->
<child v-bubble:child.click.focus.blur/>
<child v-bubble:child="'click, focus, blur'"/>
Vue.directive('bubble', {
bind(el, { value, arg: prefix = '', modifiers }, {context, componentInstance}) {
const events = value && value.trim() ? value.split(',') : Object.keys(modifiers);
if (!events.length && prefix) {
events.push(prefix);
prefix = '';
} else if(prefix) {
prefix = prefix === '_' ? '' : prefix += '-';
}
if (!componentInstance || !context || !events.length) {
return;
}
events.forEach((v: string) => {
v = v.trim();
const eventName = `${prefix}${v}`;
const bubble = context.$emit.bind(context, eventName);
componentInstance.$on(v, bubble);
});
}
});

Categories