I'm working with 3 VUE nested components (main, parent and child) and I'm getting trouble passing data.
The main component useget a simple API data based on input request: the result is used to get other info in other component.
For example first API return the regione "DE", the first component is populated then try to get the "recipes" from region "DE" but something goes wrong: The debug comments in console are in bad order and the variable used results empty in the second request (step3):
app.js:2878 Step_1: DE
app.js:3114 Step_3: 0
app.js:2890 Step_2: DE
This is the parent (included in main component) code:
parent template:
<template>
<div>
<recipes :region="region"/>
</div>
</template>
parent code:
data: function () {
return {
region: null,
}
},
beforeRouteEnter(to, from, next) {
getData(to.params.e_title, (err, data) => {
console.log("Step_1: "+data.region); // return Step_1: DE
// here I ned to update the region value to "DE"
next(vm => vm.setRegionData(err, data));
});
},
methods: {
setRegionData(err, data) {
if (err) {
this.error = err.toString();
} else {
console.log("Step_2: " + data.region); // return DE
this.region = data.region;
}
}
},
child template:
<template>
<div v-if="recipes" class="content">
<div class="row">
<recipe-comp v-for="(recipe, index) in recipes" :key="index" :title="recipe.title" :vote="recipe.votes">
</recipe-comp>
</div>
</div>
</template>
child code:
props: ['region'],
....
beforeMount () {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
},
The issue should be into parent beforeRouteEnter hook I think.
Important debug notes:
1) It looks like the child code works properly because if I replace the default value in parent data to 'IT' instead of null the child component returns the correct recipes from second API request. This confirms the default data is updated too late and not when it got results from first API request.
data: function () {
return {
region: 'IT',
}
},
2) If I use {{region}} in child template it shows the correct (and updated) data: 'DE'!
I need fresh eyes to fix it. Can you help me?
Instead of using the beforeMount hook inside of the child component, you should be able to accomplish this using the watch property. I believe this is happening because the beforeMount hook is fired before the parent is able to set that property.
More on the Vue lifecycle can be found here
More on the beforeMount lifecycle hook can be found here
In short, you can try changing this:
props: ['region'],
....
beforeMount () {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
},
To something like this:
props: ['region'],
....
watch: {
region() {
console.log("Step_3 "+this.region); // Return null!!
this.fetchData()
}
},
Cheers!!
Related
I've a hard time in understanding the methods of vue. In my put-request users can edit, delete images. In parent component the get-request loads the images and the are pushed to an image-gallery (the child-component) via properties. In my set up the console.log is always empty.
//PARENT COMPONENT
<template>
<div class="form-group">
<image-gallery :serverData="serverMap"/>
</div>
</template>
<script>
import ImageGallery from './ImageGallery.vue';
export default {
components:{ImageGallery},
data: () => ({
serverMap: {
title: '',
file: ''
}
}),
mounted () {
//AJAX ETC get servermap
.then((response) => {
this.serverMap = response.data
})
}
Just a normal straight parent-child situation. Here under the child-component
<template>
</template>
<script>
export default {
name: 'ImageGallery',
//incoming data
props: {
serverData: {
type: Object,
default () {
return {
hasLabels: true,
isHorizontal: false
}
}
}
},
created: function () {
this.loadImages()
},
methods: {
loadImages () {
console.log(this.serverData.file)
//do something with the serverData
//prepare for fileReader function
//together with new image validation
}
}
The method 'loadImages' should be automatically delevering the serverData via computed.But is doesn t. Who can help?
There is race condition.
Either not render a child until data is available; serverMap needs to be null instead of empty object in order to be distinguished from populated object:
<image-gallery v-if="serverMap" :serverData="serverMap"/>
Or delay data access in a child until it's available instead of doing this immediately in created:
watch: {
serverData(data) {
if (data)
this.loadImages()
}
}
I am using Laravel 7 and Vue.js 2.
I want to pass an object of validation errors from the catch block of an axios call to a parent component but for some reasons it doesn't work.
This is the code of the axios call:
runReport: function() {
let self = this;
const url = "api/get_report?room="+this.formReport['room']+"&participant="+this.formReport['participant']+"&start="+this.formReport['start']+"&end="+this.formReport['end'];
axios.get(url)
.then((response) => {
console.log(response.data.data);
this.meetingsReport = response.data.data;
this.$emit('passMeetings', this.meetingsReport);
this.$emit('success');
this.errors = {};
})
.catch(function(error) {
console.log(error.response.data);
self.errors = error.response.data;
alert(self.errors);
self.$emit('failure');
self.$emit('passErrors', self.errors); //problem
console.log('call ended');
});
}
This is the code in the parent component:
<template>
<div>
<report-meeting #passMeetings="onPassMeetings" #failure="displayTable=false" #success="displayTable=true"></report-meeting>
<hr>
<validated-errors :errorsMeeting="errorsMeeting" #passErrors="onPassErrors" v-if="displayTable===false"></validated-errors>
<table-report :meetingsSelected="meetingsSelected" v-if="displayTable===true"></table-report>
</div>
</template>
<script>
import TableReport from "./TableReport.vue"
import ReportMeeting from "./ReportMeeting.vue"
import ValidatedErrors from "./ValidatedErrors.vue"
export default {
components: {
'table-report': TableReport,
'report-meeting': ReportMeeting,
'validated-errors': ValidatedErrors
},
mounted() {
console.log('Component mounted.');
},
data: function() {
return {
displayTable: false,
meetingsSelected: {},
errorsMeeting: {}
}
},
methods: {
onPassMeetings(value) {
console.log(value);
this.meetingsSelected = value;
},
onPassErrors(value) {
console.log('errors passed'); //never used
this.errorsMeeting = value;
}
}
}
</script>
In the console I visualize no errors (except an 422 Unprocessable Entity). The strange thing is that the first emit works (failure), but the second one doesn't work (passErrors).
In the parent function onPassErrors I put a console.log that is never used so I suppose that the function is never called.
Can help?
This is likely caused by an event name mismatch, which can occur when using in-DOM templates because HTML attributes are automatically lower-cased (#passErrors becomes #passerrors in the DOM).
When using the development build of Vue, you'd see a warning in the browser's console:
[Vue tip]: Event "passerrors" is emitted in component but the handler is registered for "passErrors". Note that HTML attributes are case-insensitive and you cannot use v-on to listen to camelCase events when using in-DOM templates. You should probably use "pass-errors" instead of "passErrors".
This is not a problem in single file components (demo 1) or string templates (demo 2), but if you must stick with in-DOM templates, custom event names are recommended to be kebab-case:
<!-- Parent.vue -->
<MyComponent #pass-errors="onPassEvent" />
// MyComponent.vue
runReport() {
this.$emit('pass-errors', /*...*/)
}
demo 3
This is driving me nuts!
//ProfilePage.vue
<template>
<div>
<p>{{ this.$data.profile.desc }}</p>
<profileImage v-bind:profile="profile"></profileImage>
<profileText v-bind:profile="profile" v-on:updateData="updateDesc"></profileText>
</div>
</template>
<script>
import profileText from './ProfileText.vue';
import profileImage from './ProfileImage.vue';
export default {
name: 'profilePage',
component: {
profileText,
profileImage
},
data() {
return {
profile: {
image: '',
desc: ''
}
}
},
created() {
this.fetchProfile();
},
methods: {
async fetchProfile() {
const uri = 'http://localhost:8000/api/......get';
const response = await axios.get(uri);
.then(response => this.updateProfileData(response.data))
},
updateProfileData(data) {
this.$data.profile.image = data['image'];
this.$data.profile.desc = data['description'];
},
updateDesc(data) {
this.$data.profile.desc = data.desc;
},
}
}
</script>
<style scoped>
</style>
In the above .vue file. I execute a fetch to the back end which successfully returns the correct data from the DB. I successfully save the data returned to the data() part of the file. Next I import a component (the code for which is below) from the correct page, add it as a component and add it to the template and use v-bind to pass in profile from the data() part of this page. Now the imported/child component looks like this:
//ProfileText.vue
<template>
<div>
<form #submit="update">
<textarea v-model="description"></textarea>
<button type="submit">Submit</button>
</form>
<div>
<template>
<script>
export default{
name: "profileText",
props: ["profile"],
data() {
return {
description: this.$props.profile.desc
}
},
methods: {
update(e) {
e.preventDefault();
const newData = {
desc: this.$data.description
}
this.$emit('updateData', newData);
}
}
}
</script>
<style scoped>
</style>
I use v-model to bind the contents of "description" in data() to the contents of the textarea. I have it so when i edit the text area and click submit the function emits the data to the parent component which triggers a function that updates the parent data() with the new data from the text area of this component. This parts works perfectly.
However, the part I can't figure out is when the parent component executes the fetch and binds the response with the child component, why isn't the response showing up in the textarea when it loads.
I have done the exact same thing with another lot of components and it works fine on that lot. The only difference there is that with that lot the execute function brings back a response with an array of data and I use v-for(x in xs) and then bind the attributes of data() with the component x. That's the only difference. What am I missing in the code above to load the data sent in "profile" from the parent component with v-bind to the textarea in the child component with v-model. In data() i have it to return description: this.$props.profile.desc, but it is not initialising description with profile.desc - Going nuts here $#! I've been staring at the code for two days straight trying different things.
mounted Function
Called after the instance has been mounted, where el is replaced by
the newly created vm.$el. If the root instance is mounted to an
in-document element, vm.$el will also be in-document when mounted is
called.
Note that mounted does not guarantee that all child components have
also been mounted. If you want to wait until the entire view has been
rendered, you can use vm.$nextTick inside of mounted:
mounted: function () { console.log('component mounted'); }
This hook is not called during server-side rendering.
Source
Component Lifecycle
Few things:
Your syntax has errors in the ProfileText.vue file. Missing closing template and div tags
<template>
<div>
<form #submit="update">
<textarea v-model="description"></textarea>
<button type="submit">Submit</button>
</form>
</div>
</template>
You are mixing async/await and .then(). It should be:
async fetchProfile() {
const uri = 'http://localhost:8000/api/......get';
const response = await axios.get(uri);
this.updateProfileData(response.data)
},
Here are my components. I've successfully emitted parameters from the child component to the parent, but not from the parent to the child. The console.log('In initDetail()'); line fires, which means the $on event is triggered, but param is undefined. I'm wondering why.
Parent component:
components: {
"child-component": ChildComponent
}
methods: {
loadDetail() {
this.$emit('loadDetailEvent', 'test');
}
}
Child Component:
mounted() {
this.$on('loadDetailEvent', this.initDetail(param));
}
methods: {
initDetail(param) {
console.log('In initDetail()');
console.log(param);
}
}
I've also tried calling a function right away. Neither the console.log('$on event'); nor the console.log(param); lines print.
this.$on('loadDetailEvent', function(param){
console.log('$on event');
console.log(param);
});
As pointed out in the comments, there are a couple misconceptions in your code:
Your parent component is emitting a loadDetailEvent event when the loadDetail method is called. Your child component is also listening for a loadDetailEvent event, but it will only handle this event if it is emitted by its own Vue instance. If the Vue instance of the parent emits an event, the child will have no knowledge of it.
You are attempting to handle the loadDetailEvent by firing initDetail and passing the parameters of the event to that method. But, in your code you are simply setting the loadDetailEvent handler to be the result of calling this.initDetail(param) within the scope of the mounted hook. What you would want to do in this case would be to specify an anonymous function as the handler, which receives the param value and passes it to this.initDetail:
this.$on('loadDetailEvent', (param) => this.initDetail(param))
But, to get at the root of your issue: it seems like you want to call a child method when a parent method is called. You could do this a few different ways:
As #Ohgodwhy suggested, you could create a separate, global Vue instance to use as an event bus.
You could set a ref (say ref="child") on the child component tag in your parent's template and then call the child component's method directly via this.$refs.child.initDetail('test').
As #B.Fleming suggested, you could set a watcher on the child component to react to a changing prop value passed by the parent component. This gets a little tricky since you would need to maintain the value of the variable being passed as the prop (I would use a .sync modifier, as you can see below).
Here are example snippets of the above solutions:
Using an event bus:
let EventBus = new Vue();
Vue.component('child', {
template: `<div>Child</div>`,
mounted() {
EventBus.$on('loadDetailEvent', (param) => this.initDetail(param));
},
methods: {
initDetail(param) {
console.log('In initDetail()');
console.log(param);
}
}
});
new Vue({
el: '#app',
methods: {
loadDetail() {
EventBus.$emit('loadDetailEvent', 'test');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<child></child>
<button #click="loadDetail">load</button>
</div>
Using a ref:
Vue.component('child', {
template: `<div>Child</div>`,
methods: {
initDetail(param) {
console.log('In initDetail()');
console.log(param);
}
}
});
new Vue({
el: '#app',
methods: {
loadDetail() {
this.$refs.child.initDetail('test');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<child ref="child"></child>
<button #click="loadDetail">load</button>
</div>
Using a prop value and a watcher:
Vue.component('child', {
template: `<div>Child</div>`,
props: { loading: String },
methods: {
initDetail(param) {
console.log('In initDetail()');
console.log(param);
}
},
watch: {
loading(val) {
if (val) {
this.initDetail(val);
this.$emit('update:loading', '');
}
}
}
});
new Vue({
el: '#app',
data() {
return { payload: '' }
},
methods: {
loadDetail() {
this.payload = 'test';
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<child :loading.sync="payload"></child>
<button #click="loadDetail">load</button>
</div>
What is the best way to make an ajax call in the parent and then run the success function within the child?
For example:
//Parent
var ajax = myAjaxCall.fetch(); //returns a promise
//Component
ajax.success({... //etc.
Also the component should watch if ajax is run again sp it can update again if parents makes that call again.
You can add a ref attribute to the child component and then call the child's method from the parent component's scope:
In the child component:
methods: {
successHandler(promise) {
promise.success(() => { });
}
}
In the parent component:
methods: {
fetch() {
var ajax = myAjaxCall.fetch();
this.$refs.child.successHandler(ajax);
}
}
In the template:
<child ref="child"></child>
Parent template:
<div>
<button #click="executeRequest">Execute</button>
<child-component :result="result">
</div>
Parent script:
data: {
result: null
},
methods: {
executeRequest () {
// execute AJAX request using axios, fetch, whatever
this.result = response
}
}
Child template:
<div>
<div v-if="result">Call was completed and result was: {{result}}</div>
</div>
Child script:
props: {
result: Object // or string, int, whatever the AJAX request response returns
}