Adding classes to body using lifecycle hooks in vueJS - vue.js

I use beforeCreate and beforeDestroy hooks in order to add classes to body. In some cases I need to add classes, in some not.
So I have such code in each component which requires this functionality:
beforeCreate() {
document.body.classList.add('has-background')
},
beforeDestroy() {
document.body.classList.remove('has-background')
}
The problem is that if I navigate from one route to another, say from A component to B component, the beforeCreate of the B component executed first, and then beforeDestroy of the A component, which removes the has-background class.
How can I solve the issue?

Try using nextTick()
beforeCreate() {
this.$nextTick().then(() => document.body.classList.add('has-background'))
},
Edit:
I also suggest to use created() rather than beforeCreated(). But to achieve the best behavior, it is best to use mounted()

I have the same issue on my project and I have applied something like this.
methods: {
toggleBodyClass(addRemoveClass, className) {
const el = document.body;
if (addRemoveClass === 'addClass') {
el.classList.add(className);
} else {
el.classList.remove(className);
}
},
},
mounted() {
this.toggleBodyClass('addClass', 'mb-0');
},
destroyed() {
this.toggleBodyClass('removeClass', 'mb-0');
}

Related

How to use async/await in vue lifecycle hooks with vuex?

When I dispatch an action in App.vue component in mounted() lifecycle hook, it runs after other components load. I am using async/await in my action and mounted lifecycle hook.
App.vue file
methods: {
...mapActions({
setUsers: "setUsers",
}),
},
async mounted() {
try {
await this.setUsers();
} catch (error) {
if (error) {
console.log(error);
}
}
},
action.js file:
async setUsers(context) {
try {
const response = await axios.get('/get-users');
console.log('setting users');
if (response.data.success) {
context.commit('setUsers', {
data: response.data.data,
});
}
} catch (error) {
if (error) {
throw error;
}
}
},
In Users list component, I need to get users from vuex. So I am using mapGetters to get Users list.
...mapGetters({
getUsers: "getUsers",
}),
mounted() {
console.log(this.getUsers);
},
But the problem is "setting users" console log in running after console logging the this.getUsers.
In Users list component, I can use getUsers in the template but when I try to console log this.getUsers it gives nothing.
How can I run app.vue file before running any other components?
You are using async await correctly in your components. It's important to understand that async await does not hold off the execution of your component, and your component will still render and go through the different lifecycle hooks such as mounted.
What async await does is hold off the execution of the current context, if you're using it inside a function, the code after the await will happen after the promise resolves, and in your case you're using it in the created lifecycle hook, which means that the code inside the mounted lifecycle hook which is a function, will get resolved after the await.
So what you want to do, is to make sure you render a component only when data is received.
Here's how to do it:
If the component is a child component of the parent, you can use v-if, then when the data comes set data to true, like this:
data() {
return {
hasData: false,
}
}
async mounted() {
const users = await fetchUsers()
this.hasData = true;
}
<SomeComponent v-if="hasData" />
If the component is not a child of the parent, you can use a watcher to let you know when the component has rendered. When using watch you can to be careful because it will happen every time a change happens.
A simple rule of thumb is to use watch with variables that don't change often, if the data you're getting is mostly read only you can use the data, if not you can add a property to Vuex such as loadingUsers.
Here's an example of how to do this:
data: {
return {
hasData: false,
}
},
computed: {
isLoading() {
return this.$store.state.app.users;
}
},
watch: {
isLoading(isLoading) {
if (!isLoading) {
this.hasData = true;
}
}
}
<SomeComponent v-if="hasData" />
if you're fetching a data from an API, then it is better to dispatch the action inside of created where the DOM is not yet rendered but you can still use "this" instead of mounted. Here is an example if you're working with Vuex modules:
created() {
this.fetchUsers();
},
methods: {
async fetchUsers() {
await this.$store.dispatch('user/setUsers');
},
},
computed: {
usersGetters() {
// getters here
},
},
Question: Do you expect to run await this.setUsers(); every time when the app is loaded (no matter which page/component is being shown)?
If so, then your App.vue is fine. And in your 'Users list component' it's also fine to use mapGetters to get the values (note it should be in computed). The problem is that you should 'wait' for the setUsers action to complete first, so that your getUsers in the component can have value.
A easy way to fix this is using Conditional Rendering and only renders component when getUsers is defined. Possibly you can add a v-if to your parent component of 'Users list component' and only loads it when v-if="getUsers" is true. Then your mounted logic would also work fine (as the data is already there).

Where to store the object with methods in Vue?

What's the right way to store complex objects with methods in Vue? For example, the following object:
const behavior = {
onClick() {
console.log('click')
},
onDoubleClick() {
console.log('double click');
},
onMounted() {
console.log('mounted')
},
beforeMounted() {
console.log('before mounted')
},
// and so on
}
It's needed to pass to my custom component:
<custom-component :behavior="behavior"/>
It's need to note: it's not the appropriate approach to pass each method through props, because behavior might change dynamically.
You should probably use mixins for this. Check out the docs.

Vue Test Utils - Skip created hook

I want to skip all of the methods that are being called within the created() hook. Is there a way to do this?
So instead of this
created() {
this.getAllocations();
this.getModels();
this.getTeams();
this.getCustodians();
this.getDefaultFeeStructure();
}
I want this
created() { }
It's worth noting, I cannot actually change the component itself, but for testing purposes, this needs to be done.
You can accomplish this with a global mixin (see https://v2.vuejs.org/v2/guide/mixins.html#Global-Mixin)
However, for your case you need a custom merge strategy to prevent the created hook on the component from being run:
Hook functions with the same name are merged into an array so that all of them will be called. Mixin hooks will be called before the component’s own hooks. (https://v2.vuejs.org/v2/guide/mixins.html#Option-Merging)
See a working example at https://jsfiddle.net/rushimusmaximus/9akf641z/3/
Vue.mixin({
created() {
console.log("created() in global mixin")
}
});
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = (parent, child) => {
return mergeCreatedStrategy(parent);
};
new Vue ({
el: "#vue-app",
template: '<p>See console output for logging. Rendered at {{renderDate}}</p>',
data() {
return {
renderDate: new Date()
}
},
created() {
console.log("created() in component")
}
})

Validate form input fields in child component from a parent component with Vuelidate

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())
...

Can I handle back button within methods in vuejs 2?

I need some help in vuejs 2. I want to detect back button pressed event. I did some research and found this,
document.addEventListener("backbutton", yourCallBackFunction, false");
I think it is global event. I need something local, within a method. where i can use some logic.
methods: {
backButtonPressed() {
}
}
Or can i bind the global one to local function? Can anyone help me with that? TIA
Add the event on your mounted method on your root Vue component (the one the Vue instance is tied to.
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
yourCallBackFunction () {
// Your logic
}
}
mounted () {
document.addEventListener("backbutton", this.yourCallBackFunction, false);
},
beforeDestroy () {
document.removeEventListener("backbutton", this.yourCallBackFunction);
}
})
We also remove it on beforeDestroy to keep things tidy.
Note: I've not personally used the back button event so have added it to this example only because you say it's working but need a way to globally handle it. This code will do just that.
Note: As per #Luke's comment - we add the listener in the mounted event so it doesn't execute for in the SSR context. If SSR isn't a factor / consideration then we can use the created lifecycle hook.
If still someone come across this issue.
A solution for an event listener for browser-back is https://developer.mozilla.org/de/docs/Web/API/WindowEventHandlers/onpopstate
window.onpopstate = function() {
alert('browser-back');
};
Is easy, if you need to catch that behavior only your component, you can use beforeRouteLeave function in the root of your component.
Example:
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave?)
if (answer) {
next()
} else {
next(false)
}
}
But if you need to add this behavior globally, you need catch with beforeEnter in the routes.
If you are using vue-router(no idea if you don't, why...) a good solution is to use in your component:
beforeRouteLeave(to, from, next) {
if (from.name === 'nameOfFromRoute' && to.name === 'nameOfToRoute' ) {
next(false);
} else {
next();
}
console.log({ to, from });
},
This was one variation I found to work as well, a little less verbose and uses router.push in the beforeDestroy lifecycle method
Listen for popstate
Push the desired name/path to redirect
The code below would be a better understanding.
beforeDestroy() {
window.addEventListener("popstate", (event) => {
this.$router.push({ path: <your path> });
});
},
This implementation was on Nuxt 2.14.6 and works just as well with all versions of Vue.
I have a similar problem and solved using #click="backFunction"
and created the function on methods like this:
methods: {
backFunction(){
//yourlogic
},