VueJS Adding to lifecycle hooks on every component - vue.js

So I have a loader screen in my app, and the idea is to show the loader screen on the beforeCreate hook so the user can't see the stuff being rendered, and then on the mounted hook remove the loader screen.
This is fun and nice for when you have two or three view/components, but currently my app has a lot more than that, and adding it to each component/view doesn't make much sense for me.
So I was wondering, is there any way to add something to the beforeCreate and mounted hooks on a global scope. Something like this:
main.js
Vue.beforeCreate(() => {
//Show the loader screen
});
Vue.mounted(() => {
//Hide the loader screen
});
That way it would be applied to every component and view

You can use mixins for this purposes, and import in components.
//mixins.js
export default {
beforeCreate() {},
mounted() {}
}
And in component add mixins: [importedMixins]
You will have access to 'this'.
Actualy you can use and vuex to (mapGetters, mapActions etc.)
If you don't want include mixins in every component, try to use vue plugins system (https://v2.vuejs.org/v2/guide/plugins.html):
MyPlugin.install = function (Vue, options) {
// 1. add global method or property
Vue.myGlobalMethod = function () {
// something logic ...
}
// 2. add a global asset
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// something logic ...
}
...
})
// 3. inject some component options
Vue.mixin({
created: function () {
// something logic ...
}
...
})
// 4. add an instance method
Vue.prototype.$myMethod = function (methodOptions) {
// something logic ...
}
}
And use your plugin like this Vue.use(MyPlugin, { someOption: true })

There is something very silimar to your request in vue-router. I've never used afterEach but beforeEach works perfectly.
router.beforeEach((to, from, next) => {
/* must call `next` */
})
router.beforeResolve((to, from, next) => {
/* must call `next` */
})
router.afterEach((to, from) => {})
Here is a documentation
There is also a hook called 'beforeRouteEnter'.
Link to beforeRouteEnter docs

Related

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")
}
})

Testing with `Created` hook with `vue-test-utils` and `jest`

I have a Vue page like this:
<template>
</template>
<script>
created(){
this.doSomething();
}
methods: {
doSomething() {
.....
}
}
</script>
Now, we want to the testing of this created hook and check that doSomething() method is called.
Tried like this, jest is also imported in package.json
import {
shallowMount,
createLocalVue,
} from '#vue/test-utils';
const localVue = createLocalVue();
import Xyx from '/Xyx.vue';
const init = () => {
wrapper = shallowMount(Xyx, { localVue });
cmp = wrapper.vm;
};
describe('#created', () => {
it('#doSomething', () => {
init();
wrapper.setMethods({
doSomething: jest.fn(),
})
expect(cmp.doSomething).toHaveBeenCalled();
});
});
Can I do the unit test case of this created hook?
The methods option was deprecated in v1 of #vue/test-utils, so the accepted answer no longer works. I ran into this issue myself and decided to dig into the source to figure out how I could test this.
It looks like Vue actually stores all the hooks in the $options property. Each hook has an option that is an array of functions. It is important to note that a context is not bound to said functions, so you will need to use call or apply to execute them.
vm.$options.created.forEach(hook => {
hook.call(vm);
});
Because your method is called on created, it is run before you are setting the mock. Therefore, your test will fail.
You have to replace the method with the mock on initialization (in your case, on shallowMount):
describe('Xyz', () => {
it('should call doSomething() when created', () => {
const doSomething = jest.fn()
wrapper = shallowMount(Xyz, {
localvue,
methods: { doSomething }
});
expect(doSomething).toHaveBeenCalled();
});
});
Sidenote: you're not declaring cmp. At the start of your test, you should have a let cmp;
A very similar discussion here. Above the linked comment there's a method to mock properties of most Vue component lifecycle hooks.
It's possible to call hooks when we need in our tests. For example if we need to mock some data before calling a hook.
import App from '#/App.vue';
// in test
App.created.call(wrapper.vm);
Also in Typescript if we use vue-property-decorator it changes the shape of component, so needs to be done like this:
App.extendOptions.created.call(wrapper.vm)

Vue js, cannot read property $el

I have the problem with correctly understood the flow elements, method calling in vue js. It is the standard idea - fetching some data from rest api, and render it on the browser.
The getting method I wrote into mounted(). Also I added there calling renderHomePageMethod(). This method was written in methods:
mounted() {
axios.get("http://localhost:3000/api/test").then(response => {
this.testData= response.data
this.renderHomePageMethod();
});
}
In renderHomePageMethod() I used this.refs$ and $el. And probably there is the problem, everything is working fine, but in the browser I got warning about:
Uncaught (in promise) TypeError: Cannot read property '$el' of undefined
Probably I should calling
this.renderHomePageMethod()
in another place. But where?
It seems like your referenced component is not rendered before the main component renders, so it gives a reference error.
A hackish way would be something like this:
mounted() {
axios.get("http://localhost:3000/api/test").then(response => {
this.testData= response.data
setTimeout(() => {
this.renderHomePageMethod();
}, 1000); // or any other minimum delay before the subcomponent is rendered
});
}
or the better and harder way, create an event-bus.js file which contains:
import Vue from 'vue';
export const EventBus = new Vue();
in your main and sub components:
import { EventBus } from "./event-bus.js";
in your sub component, this will send the notification to the main component when it's ready to roll:
mounted(){
EventBus.$emit("subcomponent:is-mounted");
}
in your main component:
data(){
return {
testData: null
}
},
mounted(){
axios.get("http://localhost:3000/api/test").then(response => {
this.testData= response.data
});
EventBus.$on("subcomponent:is-mounted", () =>{
this.renderHomePageMethod();
});
},
beforeDestroy(){
EventBus.$off("subcomponent:is-mounted");
// don't forget to remove the listeners because of duplicate listeners may occur
// if your component refreshes (remounts)
}

dynamic importing components based on route

I'm new to VueJS and I haven't found a possibility to load components based on route. For example:
page/:pageid
page/one
page/two
I have a component Page.vue
Within that component, I watch route changes. If the route is $pageid, then import and load component $pageid.
I've read this documentation: https://v2.vuejs.org/v2/guide/components-dynamic-async.html. But that's more focussed on lazy-loading. I don't see an example for dynamic importing and loading.
Regards, Peter
According to Dynamic Route Matching of vue router, you can access the url parameters via the params property of the $route object. In your case it would be $route.params.pageid so you can use it to dynamically change the content base on the pageid parameter in the url. Also note that on url change from say in your case page/one to page/two the same component would be used, so you would have to watch the $route object change and change your content dynamically.
watch: {
'$route' (to, from) {
// react to route changes...
}
}
Vue allows you to define your component as a factory function that
asynchronously resolves your component definition.
Since import() returns a promise, so you can register your async component by using:
export default {
components: {
'Alfa': () => import('#/components/Alfa'),
'Bravo': () => import('#/components/Bravo'),
'Charlie': () => import('#/components/Charlie')
}
}
Vue will only trigger the factory function when the component needs to
be rendered and will cache the result for future re-renders.
So your component will be load only when it need to be render.
And you can use dynamic component to render it by using:
<component :is='page'/>
and
export default {
computed: {
page () {
return 'Alfa'
}
}
}
If you already using vue-router you can directly use this in routes definition. See more in document here.
const router = new VueRouter({
routes: [{
path: '/alfa',
component: () => import('#/components/Alfa')
}, {
path: '/bravo',
component: () => import('#/components/Bravo')
}, {
path: '/charlie',
component: () => import('#/components/Charlie')
}]
})
As you can see this is dynamic importing but static registration (you have to provide the path to component.) which fit mostly in many situations. But if you want to use dynamic registration, you can return component directly instead of name see document here.
export default {
computed: {
page () {
return () => import('#/components/Alfa')
}
}
}

Vue test-utils how to test a router.push()

In my component , I have a method which will execute a router.push()
import router from "#/router";
// ...
export default {
// ...
methods: {
closeAlert: function() {
if (this.msgTypeContactForm == "success") {
router.push("/home");
} else {
return;
}
},
// ....
}
}
I want to test it...
I wrote the following specs..
it("should ... go to home page", async () => {
// given
const $route = {
name: "home"
},
options = {
...
mocks: {
$route
}
};
wrapper = mount(ContactForm, options);
const closeBtn = wrapper.find(".v-alert__dismissible");
closeBtn.trigger("click");
await wrapper.vm.$nextTick();
expect(alert.attributes().style).toBe("display: none;")
// router path '/home' to be called ?
});
1 - I get an error
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:15
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property asa read-only value
2 - How I should write the expect() to be sure that this /home route has been called
thanks for feedback
You are doing something that happens to work, but I believe is wrong, and also is causing you problems to test the router. You're importing the router in your component:
import router from "#/router";
Then calling its push right away:
router.push("/home");
I don't know how exactly you're installing the router, but usually you do something like:
new Vue({
router,
store,
i18n,
}).$mount('#app');
To install Vue plugins. I bet you're already doing this (in fact, is this mechanism that expose $route to your component). In the example, a vuex store and a reference to vue-i18n are also being installed.
This will expose a $router member in all your components. Instead of importing the router and calling its push directly, you could call it from this as $router:
this.$router.push("/home");
Now, thise makes testing easier, because you can pass a fake router to your component, when testing, via the mocks property, just as you're doing with $route already:
const push = jest.fn();
const $router = {
push: jest.fn(),
}
...
mocks: {
$route,
$router,
}
And then, in your test, you assert against push having been called:
expect(push).toHaveBeenCalledWith('/the-desired-path');
Assuming that you have setup the pre-requisities correctly and similar to this
Just use
it("should ... go to home page", async () => {
const $route = {
name: "home"
}
...
// router path '/home' to be called ?
expect(wrapper.vm.$route.name).toBe($route.name)
});