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)
})
}
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
Let's take the following scenario where WelcomePage (parent) uses LoginForm (children) with a custom event #submit:
// WelcomePage.vue
<LoginForm #submit="handleLogin">Login<Button>
Then, the component LoginForm has the following code:
// LoginForm
<form #submit.prevent="handleSubmit"> ... </form>
handleSubmit() {
// do some stuff...
// following Vue docs to create custom events.
this.$emit('submit', 'some_data')
// OR... we can also use $listeners.
this.$listeners.submit('some_data')
// do other stuf...
}
Is there any con of using this.$listeners.submit() instead of this.$emit('submit')?
One advantage of using this.$listeners is it can be used with await, which is a limitation of $emit that force us to use the done() callback approach. And it's useful when we wanna update some state (this.isLoading) after the custom event is finished.
Using $emit with callback:
// LoginForm.vue
async handleSubmit() {
this.isLoading = true
this.$emit('submit', 'some_data', () => {
this.isLoading = false
})
}
// WelcomePage.vue
async handleLogin(data, done) {
// await for stuff related to "data"...
done();
}
Using $listeners with await:
// LoginForm.vue
async handleSubmit() {
this.isLoading = true
await this.$listeners.submit('some_data') // no need to use done callback
this.isLoading = false
}
So, my question is: Is it okay to use this.$listeners? What's the purpose / advantage of this.$emit?
UPDATE:
Passing a prop isLoading from the parent to the children would be the first (obvious) option, instead of using $emit.
But that would require to set and update this.isLoading = true | false on handleSubmit every time we use the children component (and it's used a lot). So I'm looking for a solution where the parent doesn't need to worry about isLoading when #submit gets called.
The way I see it is that $emit helps you keep the FLUX architecture.
You can easily see the data flow, it helps you debug.
While using $listeners on the other hand is considered as a bad practice. It can be done but it can break one way data flow. The same is with a $root, you still have access to it, but that doesn't mean you should be using (modifing) it ;-)
Still, what to use as always depends on a context and your need. Just be careful, once broken one way data flow is very hard to debug.
Edit after comment: This is just my point of view on it.
When using props and $emit as recommended way for communication between components. You have a clear data flow. Plus Vue dev-tools helps you track every $emit, so you know exactly what happend step by step.
When using "collbackFunc" as a props and call this callback in a child component that will still work. And that is still a good way to go. The downside of it is that it is not a recommended usage.
Imagin you pass that "callbackFunc" to many childs components. When something goes wrong it is very hard to track from where it was fired.
The same applies to calling directly method on $listeners. Suddenly your state is changed, and you don't know exactly from where. Which and when component has fired it.
Using props instead of callbacks from $emit
const LoginForm = {
name: 'Login-Form',
props: {
loading: {
type: Boolean,
default: false,
}
},
template: `
<button :disabled="loading" #click="$emit('some_data')">
<template v-if="loading">Logging you in</template>
<template v-else>login</template>
</button>
`,
};
new Vue({
el: '#app',
components: {
LoginForm,
},
data() {
return {
loadingLoginForm: false,
};
},
methods: {
handleSubmit() {
// Set loading state before async
this.loadingLoginForm = true;
// Some async shish
setTimeout(() => this.loadingLoginForm = false, 1500)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<login-Form :loading="loadingLoginForm" #some_data="handleSubmit" />
</div>
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
I am currently stuck at trying to show a form to edit a existing site.
My Problem is one site has about 60 Input fields and writing setters and getters for every input seems not like a good approach.
So the best thing I could think of is to save my store data to a local variable, edit the local variable and send it back.
Edit.vue
<b-form-input id="currentSiteName" v-model="this.editSite.title"></b-form-input>
...
computed: {
editSite() {
return this.$store.state.currentSite
}
},
mounted: function() {
this.$store.dispatch('SHOW_SITE', {
siteId: this.$route.params.id
});
},
Store action
SHOW_SITE: function ({ commit }, siteParams) {
http.get('/sites/' + siteParams.siteId).then((response) => {
commit('SHOW_SITE', {
site: response.data.foundSite
});
},
(err) => {
// eslint-disable-next-line
console.log(err);
})
},
Store mutations
SHOW_SITE: (state, { site }) => {
state.currentSite = site;
},
If I look in my vue-dev-tools I can see that editSite has the correct values and the values are all shown in the form but i get following two errors:
Error in event handler for "input": "TypeError: Cannot read property 'editSite' of null"
Cannot read property 'editSite' of null at callback
What I am doing wrong here or is there a better / c way to solve my problem?
Any help would be greatly appreciated!
You should use getters for access to store states.
import { mapGetters, mapActions } from 'vuex'
async mounted() {
await this.showSite(this.$route.params.id);
},
computed: {
...mapGetters([
'currentSite',
]),
},
methods: {
...mapActions([
'showSite'
]),
},
Now, this way you should able to access store states without null exception. And you should use async await for http.get. This way your code looks cleaner.
The following documentation provides a helpful explanation of what I think you are trying to achieve: https://vuex.vuejs.org/guide/forms.html
In short, bind the :value attribute of the form field to the computed property. Listen to a change event and ensure that the actual mutation of the Vuex store property happens within a Vuex mutation handler. Also, note the use of the MapState function which acts as a nice little helper to map store properties to component computed properties.
Make sure that you are thinking of the Store state prior to when the AJAX request has completed and updates the store. Setting a default state for the store will help alleviate any null references. Also worth noting that this. in the "this.editSite.title" attribute binding is unnecessary.
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