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
Related
Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource
I'm creating a react app with useState and useContext for state management. So far this worked like a charm, but now I've come across a feature that needs something like an event:
Let's say there is a ContentPage which renders a lot of content pieces. The user can scroll through this and read the content.
And there's also a BookmarkPage. Clicking on a bookmark opens the ContentPage and scrolls to the corresponding piece of content.
This scrolling to content is a one-time action. Ideally, I would like to have an event listener in my ContentPage that consumes ScrollTo(item) events. But react pretty much prevents all use of events. DOM events can't be caught in the virtual dom and it's not possible to create custom synthetic events.
Also, the command "open up content piece XYZ" can come from many parts in the component tree (the example doesn't completely fit what I'm trying to implement). An event that just bubbles up the tree wouldn't solve the problem.
So I guess the react way is to somehow represent this event with the app state?
I have a workaround solution but it's hacky and has a problem (which is why I'm posting this question):
export interface MessageQueue{
messages: number[],
push:(num: number)=>void,
pop:()=>number
}
const defaultMessageQueue{
messages:[],
push: (num:number) => {throw new Error("don't use default");},
pop: () => {throw new Error("don't use default");}
}
export const MessageQueueContext = React.createContext<MessageQueue>(defaultMessageQueue);
In the component I'm providing this with:
const [messages, setmessages] = useState<number[]>([]);
//...
<MessageQueueContext.Provider value={{
messages: messages,
push:(num:number)=>{
setmessages([...messages, num]);
},
pop:()=>{
if(messages.length==0)return;
const message = messages[-1];
setmessages([...messages.slice(0, -1)]);
return message;
}
}}>
Now any component that needs to send or receive messages can use the Context.
Pushing a message works as expected. The Context changes and all components that use it re-render.
But popping a message also changes the context and also causes a re-render. This second re-render is wasted since there is no reason to do it.
Is there a clean way to implement actions/messages/events in a codebase that does state management with useState and useContext?
Since you're using routing in Ionic's router (React-Router), and you navigate between two pages, you can use the URL to pass params to the page:
Define the route to have an optional path param. Something like content-page/:section?
In the ContentPage, get the param (section) using React Router's useParams. Create a useEffect with section as the only changing dependency only. On first render (or if section changes) the scroll code would be called.
const { section } = useParams();
useEffect(() => {
// the code to jump to the section
}, [section]);
I am not sure why can't you use document.dispatchEvent(new CustomEvent()) with an associated eventListener.
Also if it's a matter of scrolling you can scrollIntoView using refs
I have a Vue JS project where I've referenced Bootstrap 4 in the index.html page, as well as Jquery. I also have JQuery included via npm. I know there's so much wrong with that, but I'm not sure what to do about it.
That said, I'm trying to capture/hook/listen to the modal closing event. I've created the modals as components that are only rendered if a certain object exists in the vuex store. I've tried all of the following in both the parent component and the child component that is the modal in question. Nothing I've tried so far seems to work.
Any suggestions will be most welcome.
In the tags of the component:
jq(document).ready(function() {
jq('#EditContactModal').on('hidden.bs.modal', function() {
// eslint-disable-next-line no-console
console.log('MODAL HIDDEN');
//this.ModalCanceled();
});
});
jq('#EditContactModal').on('hidden.bs.modal', function() {
// eslint-disable-next-line no-console
console.log('MODAL HIDDEN');
//this.ModalCanceled();
});
One thing I did try that sort of works is:
jq(document).ready(function() {
jq('body').on('click', '#EditContactModal', function () {
// this.Close();
this.newEmployee = null;
this.$store.dispatch('setSelectedContact', null);
});
});
However, this last bit has the interesting issue of throwing a "TypeError: Cannot read property 'dispatch' of undefined" error. It's like I can't use any of the vue objects within any of the JQuery code.
**** Edit 1 - solution per Satyam Pathak's comments ****
So Satyam's answer is exactly what I needed. I've implemented this in 2 ways and am going back and forth on which way is better practice. I honestly don't know.
The first add the following to my EditContactModal component:
created() {
jq(document).on('hide.bs.modal', () => {
// eslint-disable-next-line no-console
// console.log('MODAL HIDDEN');
this.newEmployee = null;
this.$store.dispatch('setSelectedContact', null);
});
},
beforeDestroy() {
//remove listener since it's attached to the document and will remain even after
//this component is destroyed
jq(document).off('hide.bs.modal');
},
For whatever reason, providing the '#EditContactModal' selector doesn't work. The event is never attached properly. So, I have to attach the listener to the document. This is done in the Created() event. An interesting side effect is that even though the component is destroyed because the 'setSelectedContact' mutator is called, the listener still exists. That means every time the component is recreated another one is attached. To address this, I added the code in the beforeDestroy() to unattach the listener from the document.
All of this works fine.
Another solution I tried is handling this in the parent component, which happens to be my App.vue component. Here is that code:
created() {
jq(document).on('hide.bs.modal', () => {
this.$store.dispatch('setSelectedContact', null);
this.$store.dispatch('setSelectedAddress', null);
});
}
The application has 2 components that work as modals so that's why there are 2 statements there. This seems the cleaner option of the two.
I do wish I could get the '#EditContactModal' selector working, but I'm guessing this doesn't work due to life cycle issues in how the component is rendered in App.vue. What I mean is this:
<EditContactModal v-if="this.selectedContact"></EditContactModal>
So, when there isn't a "selectedContact" object in the store, there is no EditContactModal div created on the page. For now, I think I'm fine with keeping this in the App.vue, but I'm certainly open to any further thoughts on the subject.
Lastly, I rather do like the suggestion of emitting the modal closed event to the parent. That said, I'd still have to attach to the document for the listener, which also means I'll need to unattach the listener from the component. Given that, and that I have 2 such components that do this, it's simplest to just leave the code in the App.vue component.
Suppose you have a modal component Modal and inside it you attach this listener
jq(document).ready(function() {
jq('#EditContactModal').on('hidden.bs.modal', function() {
// eslint-disable-next-line no-console
console.log('MODAL HIDDEN');
//this.ModalCanceled();
});
});
jq('#EditContactModal').on('hidden.bs.modal', function() {
// eslint-disable-next-line no-console
console.log('MODAL HIDDEN');
//this.ModalCanceled();
});
You probably doing the above attachment in created/mounted hook of the component.
this last bit has the interesting issue of throwing a "TypeError: Cannot read property 'dispatch' of undefined" error. It's like I can't use any of the vue objects within any of the JQuery code.
To fix the above problem
You need to remove dynmaic scoping from your methods so that this refers to vue to make your dispatch code work. Change function declaration to arrow functions to lexically bind it.
jq(document).ready(() =>{
jq('body').on('click', '#EditContactModal', () => {
this.newEmployee = null;
this.$store.dispatch('setSelectedContact', null);
// Now you won't get the error
});
});
Now the above correction will fix your problem of dispatching a store action.
I also want to introduce emitting events from components when you want to move up in component hierarchy
Modal component can emit an event to notify parent component about something
Ex - In your Modal component
jq(document).ready(() => {
jq('#EditContactModal').on('hidden.bs.modal', () => {
this.$emit('modalClosed')
});
});
Now this will notify your Parent component
In parent you can listen it
<Modal #modalClosed="doSomething()" />
Let me know in comments if you need any more specific information
Ok so let's say I have 2 components in vue, Parent.vue and Child.vue.
In Child.vue I have this
data () {
return {
childData
}
},
In Parent.vue I get the data from child using this
data(){
return{
dataFromChild : child.data().childData,
}
},
Everything good here but I have one problem, childData will get updated based on what the user does, how do I make so that dataFromChild updates whenever childData updates? I would prefer not to use event bus or vuex as it is overkill for my case.
JavaScript (and thus Vue.js) is event-driven. Are you sure using events are overkill for your case?
In an essence, you want to update the parent data, when the child data change. This change is an event. "Something" is happening, so your Parent can react to it.
I assume that you are setting the ref on the child component. I'd like to bring your attention to this section of the guide:
$refs are only populated after the component has been rendered, and they are not reactive.
I recommend you to use Vue.js event system:
https://v2.vuejs.org/v2/guide/components-custom-events.html
I'm looking for advice in architecting my Vue app. There's a map with stuff you can click on, and a sidepanel that shows information about what you clicked on. The side panel is wrapped in a new Vue(...) (I'm not sure what to call that - a Vue object?) Currently I attach every Vue object to window so I do stuff like (simplified):
map.on('click', e => window.sidepanel.thingName = e.feature.thingName);
Now, the sidepanel code and the map code are in different modules that otherwise have little reason to communicate.
My approach seems to work ok, but I just wonder what some better patterns would be, other than using globals.
new Vue() => called a vue instance (aka vm)
I think what you are doing is reasonable if you are working inside of constraints. There are a few alternatives
Create an event bus (which is itself a vue instance) that you can use to manage the shared events. Benefits here are that you don't have to reach into the components as deeply, but you also add complexity. https://alligator.io/vuejs/global-event-bus/
Have you considered rending this as a page with the 2 vue instances as components inside of a parent? That would allow you to have the parent take care of the state and pass it down to the components. I think the main benefit of this approach is that it will be more simple to add additional functionality.
Both of these, you would end up doing something like this in the map
map.on('click', e => emit('mapClick', e.feature));
Then in your component listen for the mapClick either on the event bus if you go route 1, or in the parent container component if you go route 2
Hope that helps, good luck!
Example of Parent, the sidepanel would emit
sidepanel = Vue.component('sidepanel')
map = Vue.component('map')
parentComponent = Vue.component('parent', {
components: { map, sidepanel },
template: `
<div>
<map #mapClick="handleMapClick" :dataAsProp="someData"></map>
<sidepanel #userClicked="handleUserClick" :dataAsProp="someData"/>
</div>
`,
data() {
return { someData: [] }
},
methods: {
handleMapClick(event, data) {
// handle your update here, save data, etc
},
handleUserClick(event, data) {
// handle your update here, save data, etc
}
}
})