Vue - I want to track user changes on a page and send those updates if they navigate away. The basic idea is
//child
beforeDestroy: function() {
var that = this;
axios.post('gate/cart.php', userUpdates)
.then(function(res) {
if (res.data.success) {
that.$emit('updateCart', res.data.cart);
//parent (App.vue)
<router-view
#updateCart="updateCart"
...
methods: {
updateCart: function(newCart) {
alert('caught');
this.cart = newCart;
The dev tools show me the emit is emitted and the correct payload (res.data.cart) is sent, but the parent method isn't called. (That alert doesn't trigger.) I know the updateCart method in the parent is working, as another component uses it fine like this with a regular method:
addToCart: function() {
var that = this;
axios.post('gate/cart.php', this.dataToSend)
.then(function(res) {
if(res.data.success === true) {
that.$emit('updateCart', res.data.cart)
that.$router.push({ path: '/product/' + that.product.id})
}
If the ajax is working, I get a correct $emit, and the target method is ok, what lifecycle hook caveat is stopping me from executing the parent method? Do you know a better way to do this? (I want to check the success of the ajax call before updating the parent data.)
Related
I have a VUE 3 application where we are experiencing some sync issues when clicking on our toggles.
When the toggle is clicked, the app is supposed to call our API with the updated list.
THE PROBLEM:
When the toggle is clicked the data within the computed property is correct. We are then emitting that data to the parent component (data is still correct when received at the parent component).
Parent component calls the API which updates the data. But in the request body, the data is not correct, that is still the old list.
If I click on one of the toggles again the data being sent is then correct from the previous update (it is one click behind).
Here are the steps the app is going through:
A vue component emits computed property to the parent component
// CHILD COMPONENT EMITTING DATA TO PARENT
____filters.vue___
<template>
<ion-toggle
#click="changedFilter"
v-model="filter.selected"
:checked="filter.selected"
slot="end">
</ion-toggle>
</template>
setup(props, context) {
const changedFilter = () => {
console.log(props.searchFiltersArr) ------> **THIS DATA IS CORRECT**
context.emit("changedFilter", props.searchFiltersArr);
};
return {
changedFilter,
filters: props.searchFiltersArr,
};
}
Parent component receives emitted data from the child and calls the API using Axios.
___ SearchFilters.vue _____
<template>
<filters
#changed-filter="updateFilter"
:searchFiltersArr="searchParameters">
</filters>
</template>
export default defineComponent({
name: "SearchFilters",
components: { Filters },
setup() {
const store = useStore();
const searchParameters = computed(() => {
return {
searchParameters: store.state.user.settings.searchParameters,
};
});
async function updateFilter(search: Array<SearchFilter>) {
const headers = {
"Content-type": "application/json",
Authorization:
"Bearer " + "21345",
};
const requestBody = {
user: {
name: "",,
email: "email#email.com",
},
searchParameters: search,
};
console.log(requestBody); -----------> **REQUEST BODY HAS THE CORRECT VALUES FROM CHILD COMPONENT**
await apiClient.put("/settings", requestBody, {
headers,
}); -----> ** REQUEST BODY IN THE ACTUAL REQUEST IS NOT THE UPDATED CONTENT **
}
return {
updateFilter,
searchParameters: searchParameters.value.searchParameters,
};
},
});
The updated data is correct all the way until we call the service. Then the body is incorrect.
How can this be?
If i wrap the axios request inside of a set timeout like so, it works.
setTimeout(function (){
axios service }, 2000)
Let me know if you need any further information.
This is just a simplified example, as this shows the problem. In the real application we are calling a vuex action which then call the api and thereafter commits the updated state. The issue is exatcly the same though.
The problem is somewhat similar to this post - ReactJS - synchronization issue with axios.post
I found a solution to the problem.
Since vue can’t guarantee the order of events (computation should run before click event), I removed the click event and added a watcher instead
I am new to Vue Js and Vuelidate. Just tried to validate form input fields from a parent component like here: https://github.com/monterail/vuelidate/issues/333
Child component in the parent:
<contact-list ref="contactList" :contacts="contacts" #primaryChanged="setPrimary" #remove="removeContact" #ready="isReady => readyToSubmit = isReady"/>
The method in the child:
computed: {
ready() {
return !this.$v.email.$invalid;
}
},
watch: {
ready(val) {
this.$emit('ready', val);
}
},
methods: {
touch() {
this.$v.email.$touch();
}
}
I'm calling the touch() method from the parent like so:
submit() {
this.$refs.contactList.touch();
},
But I get this error:
Error in event handler for "click": "TypeError: this.$refs.contactList.touch is not a function".
Any ideas? Thanks.
I was facing the same problem. Here is what I have done to solve it.
Created a global event pool. Where I can emit events using $emit and my child can subscribe using $on or $once and unsubscribe using $off. Inside your app.js paste the below code. Below is the list of event pool actions.
Emit: this.$eventPool.$emit()
On: this.$eventPool.$on()
Off: this.$eventPool.$off()
once: this.$eventPool.$once()
Vue.prototype.$eventPool = new Vue();
Inside my child components, I have created a watch on $v as below. Which emits the status of the form to the parent component.
watch: {
"$v.$invalid": function() {
this.$emit("returnStatusToParent", this.$v.$invalid);
}
}
Now inside you parent component handle the status as below.
<ChildComponent #returnStatusToParent="formStatus =>isChildReady=formStatus"></ChildComponent>
Now to display the proper errors to the users we will $touch the child form. For that, we need to emit an event in the above-created event pool and our child will subscribe to that.
parent:
this.$eventPool.$emit("touchChildForm");
child:
mounted() {
this.$eventPool.$on("touchChildForm", () => {
this.$v.$touch();
this.$emit("returnStatusToParent", this.$v.$invalid);
});
},
destroyed() {
this.$eventPool.$off("touchChildForm", () => `{});`
}
Hope it helps :)
I'm adding my answer after this question already has an accepted solution, but still hope it might help others. I have been at this for the entire week. None of the above solutions work for my scenario because there are children components nested 2 levels deep so the "ref" approach won't work when I need the utmost parent component to trigger all validations and be able to know if the form is valid.
In the end, I used vuex with a fairly straightforward messages module. Here is that module:
const state = {
displayMessages: [],
validators: []
};
const getters = {
getDisplayMessages: state => state.displayMessages,
getValidators: state => state.validators
};
const actions = {};
const mutations = {
addDisplayMessage: (state, message) => state.displayMessages.push(message),
addValidator: (state, validator) => {
var index = 0;
while (index !== -1) {
index = _.findIndex(state.validators, searchValidator => {
return (
searchValidator.name == validator.name &&
searchValidator.id == validator.id
);
});
if (index !== -1) {
console.log(state.validators[index]);
state.validators.splice(index, 1);
}
}
state.validators.push(validator);
}
};
export default {
state,
getters,
actions,
mutations
};
Then each component has this in its mounted event:
mounted() {
this.addValidator( {name: "<name>", id: 'Home', validator: this.$v}) ;
}
Now when a user clicks the "Submit" button on the home page, I can trigger all validations like so:
this.getValidators.forEach( (v) => {
console.log(v);
v.validator.$touch();
});
I can just as easily check the $error, $invalid properties of the vuelidate objects. Based on my testing, the vuelidate reactivity remains intact so even though the objects are saved to vuex, any changes on the component fields are reflected as expected.
I plan to leave the messages and styling to convey the errors in the gui to the components themselves, but this approach lets me pause the form submission when an error occurs.
Is this a good thing to do? I honestly have no idea. The only hokey bit if having to remove validators before adding them. I think that's more an issue with my component logic, than an issue with this as a validation solution.
Given that this has taken me a whole week to arrive at, I'm more than happy with the solution, but would welcome any feedback.
Had a similar issue trying to validate child components during a form submission on the parent component. My child components are only one level deep so if you have deeper nesting this way may not work or you have to check recursively or something. There are probably better ways to check but this worked for me. Good luck.
// parent component
methods: {
onSave() {
let formIsInvalid = this.$children.some(comp => {
if (comp.$v) { // make sure the child has a validations prop
return comp.$v.$invalid
}
})
if (!formIsInvalid) {
// submit data
}
else {
// handle invalid form
}
}
I have found another solution for this validation, it's very simple. Child component in the parent:
<contact-list ref="customerContacts" :contacts="customer.contacts" />
Validations in child component:
:validator="$v.model.$each[index].name
...
validations: {
model: {
required,
$each: {
name: {
required
},
email: {
required,
email
},
phone: {
required
}
}
}
}
And on submit in the parent:
async onSubmit() {
if(this.$refs.customerContacts.valid())
...
I am using Nuxt js SSR for an app that am build, I installed Vue Event plugin but when i emit an event it runs twice at the listener. Created hook runs twice too.
Modules am using:
Axios, Auth, Toast
Child Component
methods: {
initPlaylist(songs) {
console.log(songs)
}
},
mounted () {
this.$events.$on('playAll', data => {
this.initPlaylist(data.songs) //runs twice
})
}
Parent Component
method () {
playAll (songs) {
this.$events.$emit('playAll', songs)
}
}
How can i resolve this issues guys? I need your help.
Maybe you have to call that parent's method on client side only.
you can write code like this to prevent emit on server side:
methods: {
playAll(songs) {
if (process.server) return
this.$events.$emit('playAll', songs)
}
}
or do not call playAll method on server side. (eg: created, mounted...)
You need to off that event first before.
this.$events.$off("playAll");
this.$events.$on('playAll', data => {
this.initPlaylist(data.songs) //runs twice
})
I'm trying to render my DOM, dependent on some data I'm returning from an axios get. I can't seem to get the timing right. The get is in the created hook, but there is a delay between the get and actually receiving the data. Basically if there is info in seller_id then I need to show the cancel button, otherwise don't. Here is my code:
this is in my created hook
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
and then this is the logic to show or hide the button. I've tried created, mounted, beforeUpdate, and updated all with no luck. I've also tried $nextTick but I can't get the timing correct. This is what I have currently:
beforeUpdate: function () {
// this.$nextTick(function () {
function sellerIdNotBlank() {
var valid = this.seller_id == '';
return !valid;
}
if(sellerIdNotBlank()){
this.show_cancel_button = true;
}
// })
},
First, it is pointless to get your data from backend and try to sync with Vue.js lifecycle methods. It never works.
Also, you should avoid beforeUpdate lifecycle event. It is often a code smell. beforeUpdate is to be used only when you have some DOM manipulations done manually and you need to adjust them again before Vue.js attempt to re-render.
Further, show_cancel_button is a very good candidate for a computed property. Here is how component will look:
const componentOpts = {
data() {
return {
seller_id: '',
// ... some more fields
};
},
created() {
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
},
computed: {
show_cancel_button() {
return this.seller_id !== '';
}
}
}
I have a login modal that I activate by setting .is-active to it. For this, I have a method like this:
methods: {
toggleModal: function (event) {
this.isActive = !this.isActive
}
}
that I run onclick. Depending on the boolean value of isActive, my modal gets the class .is-active.
Thing is, in my modal I have a button that takes the user to a new view which means it's rendering a new component, with this code:
<router-link class="control" #click="toggleModal()" to="/register">
As you can see, it's routing to /register. Before doing this, I need to run toggleModal() so that the modal gets closed. Right now it's routing without running the method which means that the new view has my modal overlay which is... not optimal.
Is there any good way to do this in Vue? Could I maybe create a method, that first calls toggleModal(), and then routes from the method?
Thanks.
I would define a method that calls toggleModal first, then navigates. Like so:
methods: {
navigateAway () {
this.isActive = !this.isActive
this.$router.push('/register')
}
}
You don't need the event argument unless you intend on capturing more data from the event or event target. You could also wrap the router push in a setTimeout if you so desire, for perhaps cleaner looking view changes.
methods: {
navigateAway () {
let vm = this
vm.isActive = !vm.isActive
setTimeout(function () {
vm.$router.push('/register')
}, 50)
}
}
Of course, there are hooks that you can use from vue-router that make this easy. Example (assuming you're using single file components and Vue.js 2.x):
export default {
data () {
return {
isActive: false
}
},
beforeRouteLeave (to, from, next) {
this.isActive = false // I assume that you would not want to leave it open upon navigating away
next()
}
}
Link to vue router hooks: https://router.vuejs.org/en/advanced/navigation-guards.html